howone 0.1.23 → 0.1.26

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 (36) hide show
  1. package/package.json +1 -1
  2. package/templates/vite/.howone/skills/howone/01-architect/01-app-generation.md +215 -0
  3. package/templates/vite/.howone/skills/{howone-sdk → howone}/01-architect/02-manifest-codegen.md +67 -4
  4. package/templates/vite/.howone/skills/howone/02-database/01-schema-design.md +541 -0
  5. package/templates/vite/.howone/skills/howone/02-database/02-schema-operations.md +398 -0
  6. package/templates/vite/.howone/skills/howone/02-database/03-data-access-patterns.md +309 -0
  7. package/templates/vite/.howone/skills/howone/02-database/04-query-dsl-and-responses.md +237 -0
  8. package/templates/vite/.howone/skills/howone/02-database/05-ai-persistence-patterns.md +372 -0
  9. package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/01-client-setup.md +58 -36
  10. package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/02-entity-operations.md +67 -0
  11. package/templates/vite/.howone/skills/howone/03-sdk/03-auth.md +414 -0
  12. package/templates/vite/.howone/skills/howone/03-sdk/04-react-integration.md +191 -0
  13. package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/07-ai-action-calls.md +168 -64
  14. package/templates/vite/.howone/skills/howone/03-sdk/08-extension-boundaries.md +226 -0
  15. package/templates/vite/.howone/skills/howone/04-ai/01-ai-capability-architecture.md +205 -0
  16. package/templates/vite/.howone/skills/howone/04-ai/02-workflow-contract-rules.md +426 -0
  17. package/templates/vite/.howone/skills/howone/04-ai/03-ai-sdk-handoff.md +234 -0
  18. package/templates/vite/.howone/skills/howone/04-ai/04-service-capability-catalog.md +281 -0
  19. package/templates/vite/.howone/skills/howone/04-ai/05-workflow-operations.md +256 -0
  20. package/templates/vite/.howone/skills/howone/04-ai/06-ai-feature-playbooks.md +296 -0
  21. package/templates/vite/.howone/skills/{howone-sdk → howone}/SKILL.md +29 -12
  22. package/templates/vite/.howone/skills/howone/agents/openai.yaml +4 -0
  23. package/templates/vite/package.json +1 -1
  24. package/templates/vite/.howone/skills/howone-sdk/01-architect/01-app-generation.md +0 -126
  25. package/templates/vite/.howone/skills/howone-sdk/02-database/01-schema-design.md +0 -147
  26. package/templates/vite/.howone/skills/howone-sdk/02-database/02-schema-operations.md +0 -96
  27. package/templates/vite/.howone/skills/howone-sdk/02-database/03-data-access-patterns.md +0 -172
  28. package/templates/vite/.howone/skills/howone-sdk/03-sdk/03-auth.md +0 -616
  29. package/templates/vite/.howone/skills/howone-sdk/03-sdk/04-react-integration.md +0 -398
  30. package/templates/vite/.howone/skills/howone-sdk/04-ai/.gitkeep +0 -1
  31. package/templates/vite/.howone/skills/howone-sdk/04-ai/01-ai-capability-architecture.md +0 -142
  32. package/templates/vite/.howone/skills/howone-sdk/04-ai/02-workflow-contract-rules.md +0 -169
  33. package/templates/vite/.howone/skills/howone-sdk/04-ai/03-ai-sdk-handoff.md +0 -80
  34. package/templates/vite/.howone/skills/howone-sdk/agents/openai.yaml +0 -4
  35. /package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/05-file-upload.md +0 -0
  36. /package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/06-raw-http.md +0 -0
@@ -0,0 +1,191 @@
1
+ # React Integration
2
+
3
+ ## What `@howone/sdk/react` Provides
4
+
5
+ Thin integration layer: auth context plus the HowOne floating brand button. **No** entity hooks,
6
+ AI hooks, toast system, redirect overlay, or app-owned UI.
7
+
8
+ Exports:
9
+
10
+ - `HowOneProvider`
11
+ - `useHowoneContext`
12
+ - `FloatingButton`
13
+
14
+ ---
15
+
16
+ ## Auth: one SDK config + one Provider flag
17
+
18
+ **Step 1 — `src/lib/sdk.ts` (required):**
19
+
20
+ ```ts
21
+ const client = createClient({
22
+ projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
23
+ env: import.meta.env.VITE_HOWONE_ENV,
24
+ })
25
+ ```
26
+
27
+ **Step 2 — Provider:**
28
+
29
+ ```tsx
30
+ import { HowOneProvider } from '@howone/sdk/react'
31
+ import './lib/sdk' // must import before Provider so auth config is registered
32
+
33
+ <HowOneProvider auth="none" brand="visible">
34
+ <App />
35
+ </HowOneProvider>
36
+ ```
37
+
38
+ | Layer | Setting | Meaning |
39
+ |-------|---------|---------|
40
+ | `createClient` | `{ projectId, env }` (default **hosted**) | Login/logout → HowOne `/auth` |
41
+ | `createClient` | `auth: 'custom'` | Login/logout → your `loginPath`; APIs still HowOne |
42
+ | `HowOneProvider` | `auth="required"` | Default with hosted — redirect to HowOne login |
43
+ | `HowOneProvider` | `auth="none"` | Use with `auth: 'custom'`; guard routes yourself |
44
+
45
+ Default (HowOne hosted login):
46
+
47
+ ```tsx
48
+ createClient({ projectId, env })
49
+ <HowOneProvider auth="required" />
50
+ ```
51
+
52
+ Custom login page (your UI, HowOne auth APIs, keep HowOne logo unless product asks to hide it):
53
+
54
+ ```tsx
55
+ createClient({ projectId, env, auth: 'custom', loginPath: '/login' })
56
+ <HowOneProvider auth="none" brand="visible" />
57
+ ```
58
+
59
+ ---
60
+
61
+ ## HowOneProvider
62
+
63
+ ```tsx
64
+ <HowOneProvider
65
+ auth="none"
66
+ brand="visible"
67
+ onAuthRedirect={({ mode, returnUrl }) => {
68
+ // App may set its own loading/redirect state here.
69
+ }}
70
+ onAuthStateChange={(state) => {
71
+ // App may update analytics or local UI state here.
72
+ }}
73
+ >
74
+ <App />
75
+ </HowOneProvider>
76
+ ```
77
+
78
+ ### HowOneProviderProps
79
+
80
+ ```ts
81
+ type HowOneProviderAuth = 'required' | 'optional' | 'none'
82
+
83
+ interface HowOneProviderProps {
84
+ children: React.ReactNode
85
+ projectId?: string // prefer createClient projectId
86
+ auth?: HowOneProviderAuth
87
+ brand?: 'visible' | 'hidden'
88
+ showBrandButton?: boolean
89
+ theme?: 'dark' | 'light' | 'system' | 'inherit'
90
+ onAuthStateChange?: (state: AuthState) => void
91
+ onAuthRedirect?: (info: { mode: 'hosted' | 'custom'; returnUrl: string }) => void
92
+ }
93
+ ```
94
+
95
+ **Important:** Provider `auth` is only a **route guard**. Login/logout URLs come from `createClient({ auth: 'custom' })`.
96
+
97
+ The provider must not render app-owned UI. It does not own toasts, dialogs, pages, custom login UI,
98
+ or redirect overlays. It does keep the bottom-right HowOne logo by default through `FloatingButton`.
99
+ Use `brand="hidden"` or `showBrandButton={false}` only when the product explicitly asks to hide it.
100
+
101
+ ---
102
+
103
+ ## useHowoneContext
104
+
105
+ ```ts
106
+ const { user, token, isAuthenticated, logout } = useHowoneContext()
107
+ ```
108
+
109
+ ### Logout
110
+
111
+ ```tsx
112
+ <button onClick={() => void logout()}>Sign out</button>
113
+ ```
114
+
115
+ With `auth: 'custom'`, `logout()` clears session and navigates to `loginPath` — **not** howone.dev.
116
+
117
+ Equivalent:
118
+
119
+ ```ts
120
+ await howone.auth.logout()
121
+ ```
122
+
123
+ ### Custom login page link
124
+
125
+ ```tsx
126
+ import { useNavigate } from 'react-router-dom'
127
+ import howone from '@/lib/sdk'
128
+
129
+ function Header() {
130
+ const navigate = useNavigate()
131
+ const { isAuthenticated, logout } = useHowoneContext()
132
+
133
+ if (!isAuthenticated) {
134
+ return <button onClick={() => navigate(howone.auth.loginPath)}>Sign in</button>
135
+ }
136
+
137
+ return <button onClick={() => void logout()}>Sign out</button>
138
+ }
139
+ ```
140
+
141
+ ---
142
+
143
+ ## FloatingButton
144
+
145
+ The bottom-right HowOne logo is part of the SDK React integration and should remain visible by
146
+ default. It does not replace your login page and does not perform app auth. Hide it only with
147
+ `brand="hidden"` or `showBrandButton={false}`.
148
+
149
+ ---
150
+
151
+ ## Protected route pattern
152
+
153
+ ```tsx
154
+ function ProtectedPage() {
155
+ const [user, setUser] = useState(null)
156
+ const navigate = useNavigate()
157
+
158
+ useEffect(() => {
159
+ howone.me()
160
+ .then(setUser)
161
+ .catch(() => navigate(howone.auth.loginPath, { replace: true }))
162
+ }, [navigate])
163
+
164
+ if (!user) return null
165
+ return <div>Welcome {user.name}</div>
166
+ }
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Common mistakes
172
+
173
+ | Mistake | Fix |
174
+ |---------|-----|
175
+ | Custom UI but no `auth: 'custom'` | Add to `createClient` |
176
+ | `HowOneProvider auth="required"` without custom SDK auth | Hosted redirect to howone.ai |
177
+ | `useHowoneContext` without Provider | Wrap app in `HowOneProvider` |
178
+ | Import Provider before `./lib/sdk` | Import sdk module first |
179
+ | Manual redirect to howone.dev on logout | Use `howone.auth.logout()` |
180
+ | Deleting the bottom-right HowOne logo by default | Keep `brand="visible"` unless explicitly asked to hide it |
181
+ | Expecting SDK toast APIs | Implement visible feedback in the frontend app from callbacks/results |
182
+
183
+ ---
184
+
185
+ ## Import map
186
+
187
+ | Need | Import |
188
+ |------|--------|
189
+ | Provider, context | `@howone/sdk/react` |
190
+ | Client, OTP, OAuth | `@howone/sdk` |
191
+ | App singleton | `@/lib/sdk` default export |
@@ -62,17 +62,23 @@ type AiActionConfig<TInput, TOutput> = {
62
62
  ```ts
63
63
  type AiResult = {
64
64
  success: boolean
65
- finalResult: Record<string, unknown> | null // the workflow output payload
65
+ /** Terminal outcome of the run */
66
+ outcome: 'success' | 'credit_insufficient' | 'run_error' | null
67
+ finalResult: Record<string, unknown> | null // the workflow output payload (from run_complete)
68
+ /** Accumulated state data from state_update / state_snapshot events */
69
+ stateData: Record<string, unknown>
66
70
  nodeExecutions: Array<{
67
71
  nodeName: string
68
- content: string
72
+ agentName?: string
69
73
  timestamp: number
70
74
  }>
71
- costUpdates: Array<{
72
- token: number
73
- totalToken: number
74
- cost: number
75
- totalCost: number
75
+ toolExecutions: Array<{
76
+ toolCallId: string
77
+ toolName: string
78
+ args?: Record<string, unknown>
79
+ result?: unknown
80
+ durationMs?: number
81
+ cost?: number
76
82
  timestamp: number
77
83
  }>
78
84
  totalDuration: number
@@ -92,14 +98,43 @@ type AiSession = {
92
98
 
93
99
  ### AiEvent (SSE events)
94
100
 
101
+ All events share a common base, then carry a typed `payload`:
102
+
95
103
  ```ts
96
104
  type AiEvent = {
97
- type: string
98
- data?: Record<string, unknown>
99
- [key: string]: unknown
105
+ type: string // see channel catalog below
106
+ id: string
107
+ run_id: string
108
+ workflow_id: string
109
+ timestamp: string // ISO 8601 UTC
110
+ seq: number // monotonic ordering within the run
111
+ // Context fields — present when applicable
112
+ node_name?: string
113
+ node_status?: 'pending' | 'running' | 'completed' | 'failed'
114
+ action_step?: number
115
+ agent_name?: string
116
+ action_name?: string
117
+ agent_loop_step?: number
118
+ message_id?: string
119
+ tool_call_id?: string
120
+ payload?: Record<string, unknown>
100
121
  }
101
122
  ```
102
123
 
124
+ **SSE Channels and event types:**
125
+
126
+ | Channel | Key event types |
127
+ |---|---|
128
+ | `lifecycle` | `run_start`, `run_complete`, `credit_insufficient`, `run_error` |
129
+ | `node` | `node_scheduled`, `node_start`, `node_complete`, `node_failed` |
130
+ | `action` | `action_scheduled`, `action_start`, `action_complete` |
131
+ | `messages` | `ai_message_start`, `ai_message_chunk`, `ai_message_end`, `action_output` |
132
+ | `tools` | `tool_call_start`, `tool_call_end`, `tool_call_error` |
133
+ | `state` | `state_update`, `state_snapshot` |
134
+ | `metadata` | `progress` |
135
+
136
+ Stream terminates after exactly one of: `run_complete`, `credit_insufficient`, or `run_error`.
137
+
103
138
  ---
104
139
 
105
140
  ## Defining AI Actions
@@ -195,17 +230,17 @@ When an action omits `outputSchema`, `run()` returns the raw `AiResult` executio
195
230
 
196
231
  ```ts
197
232
  const result = await howone.ai.generateStory.run(input, {
198
- onStreamChunk: (chunk) => {
199
- console.log('chunk:', chunk)
233
+ onMessageChunk: (text) => {
234
+ console.log('chunk:', text)
200
235
  },
201
- onNodeStart: (nodeName, content) => {
202
- console.log(`Node ${nodeName} started`)
236
+ onNodeStart: (event) => {
237
+ console.log(`Node ${event.node_name} started`)
203
238
  },
204
- onCostUpdate: (cost) => {
205
- console.log(`Tokens used: ${cost.totalToken}`)
239
+ onStateUpdate: (delta) => {
240
+ console.log('State delta:', delta)
206
241
  },
207
- onProgress: (progress) => {
208
- setProgress(progress)
242
+ onProgress: (percent) => {
243
+ setProgress(percent)
209
244
  },
210
245
  onError: (error) => {
211
246
  console.error('SSE error:', error)
@@ -213,13 +248,32 @@ const result = await howone.ai.generateStory.run(input, {
213
248
  })
214
249
  ```
215
250
 
251
+ UI feedback belongs in the frontend app. Do not import or expect SDK toast APIs. Use returned
252
+ promises and callbacks to update app-owned state:
253
+
254
+ ```ts
255
+ setStatus({ type: 'loading', message: 'Generating story...' })
256
+
257
+ try {
258
+ const output = await howone.ai.generateStory.run(input, {
259
+ onProgress: (progress) => setProgress(progress),
260
+ })
261
+ setStatus({ type: 'success', message: 'Story ready', output })
262
+ } catch (error) {
263
+ setStatus({
264
+ type: 'error',
265
+ message: error instanceof Error ? error.message : 'Story generation failed',
266
+ })
267
+ }
268
+ ```
269
+
216
270
  ### stream() — start and control a session
217
271
 
218
272
  ```ts
219
273
  function startStream(input: GenerateStoryInput) {
220
274
  const session = howone.ai.generateStory.stream(input, {
221
- onStreamChunk: (chunk) => {
222
- setOutput(prev => prev + chunk)
275
+ onMessageChunk: (text) => {
276
+ setOutput(prev => prev + text)
223
277
  },
224
278
  onComplete: (result) => {
225
279
  console.log('Done:', result.finalResult)
@@ -248,17 +302,27 @@ const result = await session.result
248
302
  async function consumeEvents(input: GenerateStoryInput) {
249
303
  for await (const event of howone.ai.generateStory.events(input)) {
250
304
  switch (event.type) {
251
- case 'stream_content':
252
- setOutput(prev => prev + (event.data?.delta ?? ''))
305
+ case 'ai_message_chunk':
306
+ // streaming LLM text delta
307
+ setOutput(prev => prev + (event.payload?.content_block?.text ?? event.payload?.content ?? ''))
253
308
  break
254
309
  case 'node_start':
255
- console.log('Node started:', event.data?.nodeName)
310
+ console.log('Node started:', event.node_name)
311
+ break
312
+ case 'tool_call_end':
313
+ console.log('Tool result:', event.tool_call_id, event.payload?.result)
314
+ break
315
+ case 'state_update':
316
+ console.log('State delta:', event.payload?.delta)
256
317
  break
257
- case 'cost_update':
258
- console.log('Cost:', event.data?.totalCost)
318
+ case 'run_complete':
319
+ console.log('Final result:', event.payload?.result)
259
320
  break
260
- case 'complete':
261
- console.log('Final result:', event.data)
321
+ case 'credit_insufficient':
322
+ showCreditError(event.payload?.details?.reason)
323
+ break
324
+ case 'run_error':
325
+ showExecutionError(event.payload?.details?.reason)
262
326
  break
263
327
  }
264
328
  }
@@ -333,37 +397,43 @@ export const analyzeDataInputSchema = z.object({
333
397
 
334
398
  ```ts
335
399
  type SSEExecutionOptions = {
336
- // Called for every raw SSE event
337
- onEvent?: (event: { type: string; data?: Record<string, unknown> }) => void
400
+ // Called for every parsed event with its SSE channel
401
+ onEvent?: (event: AiEvent, channel: string) => void
338
402
 
339
- // Called when a workflow node starts executing
340
- onNodeStart?: (nodeName: string, content: string) => void
403
+ // Lifecycle
404
+ onRunStart?: (event: RunStartEvent) => void
405
+ onRunComplete?: (event: RunCompleteEvent, result: AiResult) => void
406
+ /** Credits / quota exhausted — show top-up UI, NOT a generic error */
407
+ onCreditInsufficient?: (event: CreditInsufficientEvent) => void
408
+ /** Workflow logic failed — show retry/support UI */
409
+ onRunError?: (event: RunErrorEvent) => void
341
410
 
342
- // Called with streaming text delta (for LLM text generation nodes)
343
- onStreamContent?: (delta: string) => void
411
+ // Nodes
412
+ onNodeStart?: (event: NodeStartEvent) => void
413
+ onNodeComplete?: (event: NodeCompleteEvent) => void
344
414
 
345
- // Called with each raw stream chunk
346
- onStreamChunk?: (chunk: string) => void
415
+ // Messages streaming LLM text
416
+ onMessageChunk?: (text: string, event: AiMessageChunkEvent) => void
417
+ onMessageEnd?: (event: AiMessageEndEvent) => void
347
418
 
348
- // Called when token/cost counters update
349
- onCostUpdate?: (cost: {
350
- token: number
351
- totalToken: number
352
- cost: number
353
- totalCost: number
354
- timestamp: number
355
- }) => void
419
+ // Tools
420
+ onToolCallStart?: (event: ToolCallStartEvent) => void
421
+ onToolCallEnd?: (event: ToolCallEndEvent) => void
422
+ onToolCallError?: (event: ToolCallErrorEvent) => void
356
423
 
357
- // Called with an estimated progress value (0–100)
358
- onProgress?: (progress: number) => void
424
+ // State live output panel updates
425
+ onStateUpdate?: (delta: Record<string, unknown>, event: StateUpdateEvent) => void
359
426
 
360
- // Called with log messages from workflow nodes
427
+ // Progress percent 0-100
428
+ onProgress?: (percent: number, message?: string) => void
429
+
430
+ // Internal transport log
361
431
  onLog?: (message: string) => void
362
432
 
363
- // Called if an error occurs
433
+ // Called on any error (credit or execution)
364
434
  onError?: (error: Error) => void
365
435
 
366
- // Called when the workflow completes with the raw execution envelope
436
+ // Called when the stream closes (success or error)
367
437
  onComplete?: (result: AiResult) => void
368
438
 
369
439
  // Abort signal — connect to an AbortController for cancellation
@@ -378,6 +448,9 @@ type SSEExecutionOptions = {
378
448
  }
379
449
  ```
380
450
 
451
+ > `onCreditInsufficient` and `onRunError` are **mutually exclusive** terminal events.
452
+ > Do not use a generic `onError` to distinguish them — use the dedicated callbacks.
453
+
381
454
  ---
382
455
 
383
456
  ## AiSchemaValidationError
@@ -403,28 +476,59 @@ try {
403
476
 
404
477
  ## AI Result Persistence
405
478
 
406
- When AI-generated content should be saved to an entity:
479
+ When AI-generated content should be saved to an entity, prefer the SDK persistence helper for
480
+ history-style products. It standardizes the pending-first pattern from `02-database/05-ai-persistence-patterns.md`
481
+ without adding UI behavior.
407
482
 
408
483
  ```ts
409
- // 1. Run the AI action
410
- const output = await howone.ai.generateStory.run({
411
- topic: 'Dragons and magic',
412
- ageRange: '6-8',
413
- })
484
+ import { runAiActionAndPersist } from '@howone/sdk'
414
485
 
415
- // 2. Save to entity
416
- const saved = await howone.entities.Story.create({
417
- title: output.title,
418
- content: output.content,
419
- authorId: currentUser.id,
420
- status: 'draft',
421
- wordCount: output.content.split(' ').length,
422
- // Track generation metadata
423
- promptTopic: 'Dragons and magic',
424
- generatedAt: new Date().toISOString(),
486
+ const result = await runAiActionAndPersist({
487
+ entity: howone.entities.Generation,
488
+ input: {
489
+ prompt: 'Dragons and magic',
490
+ ageRange: '6-8',
491
+ },
492
+ createPending: (input) => ({
493
+ prompt: input.prompt,
494
+ ageRange: input.ageRange,
495
+ status: 'pending',
496
+ requestedAt: new Date().toISOString(),
497
+ }),
498
+ run: (input) => howone.ai.generateStory.run(input),
499
+ mapCompleted: ({ output }) => ({
500
+ status: 'completed',
501
+ title: output.title,
502
+ content: output.content,
503
+ completedAt: new Date().toISOString(),
504
+ }),
505
+ mapFailed: ({ error }) => ({
506
+ status: 'failed',
507
+ errorMessage: error instanceof Error ? error.message : 'Generation failed',
508
+ }),
509
+ onStateChange: (state) => {
510
+ // app-owned UI callback; SDK does not show toasts
511
+ setGenerationState(state.status)
512
+ },
425
513
  })
426
514
  ```
427
515
 
516
+ Return shape:
517
+
518
+ ```ts
519
+ type AiPersistenceResult<TRecord, TOutput> =
520
+ | { status: 'completed'; record: TRecord; output: TOutput }
521
+ | { status: 'failed'; record: TRecord; error: unknown }
522
+ ```
523
+
524
+ Rules:
525
+
526
+ - `createPending` must only return fields declared in the entity schema.
527
+ - `mapCompleted` maps durable product fields from AI output to entity update payload.
528
+ - `mapFailed` should persist a failure state if the product shows history or retry.
529
+ - Use `onStateChange` to update app-owned UI; do not add SDK toast behavior.
530
+ - For simple one-shot AI actions that do not need history, call `howone.ai.*.run()` directly.
531
+
428
532
  ---
429
533
 
430
534
  ## React Patterns