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
@@ -108,6 +108,11 @@ Terminal result meaning:
108
108
  | `completed` | workflow operation completed |
109
109
  | `failed` / `canceled` / `timed_out` | report error and do not pretend workflow is ready |
110
110
 
111
+ A non-terminal submission is not a legal completion or pause boundary for the Coding Agent. Do not
112
+ end the run merely to wait for a host notification. Continue all implementation that does not need
113
+ the promoted workflow ID, including UI structure, persistence, loading/error states, and other app
114
+ logic. Defer only final workflow-ID binding and live execution verification until terminal success.
115
+
111
116
  ## Standard Flow
112
117
 
113
118
  New AI feature:
@@ -116,7 +121,8 @@ New AI feature:
116
121
  ai-capability-design apply_capability_patch
117
122
  sync_ai_artifacts
118
123
  external-ai-capability { cwd, capabilityNames }
119
- wait for terminal result
124
+ continue independent app implementation; do not stop to wait
125
+ terminal result
120
126
  sync_ai_artifacts
121
127
  read .howone/ai/manifest.json
122
128
  ```
@@ -127,7 +133,8 @@ Schema-changing AI update:
127
133
  ai-capability-design apply_capability_patch
128
134
  sync_ai_artifacts
129
135
  external-ai-capability { cwd, updates: [{ capabilityName, updatePrompt }] }
130
- wait for terminal result
136
+ continue independent app implementation; do not stop to wait
137
+ terminal result
131
138
  sync_ai_artifacts
132
139
  read .howone/ai/manifest.json
133
140
  ```
@@ -136,13 +143,15 @@ Behavior-only update:
136
143
 
137
144
  ```text
138
145
  external-ai-capability { cwd, updates: [{ capabilityName, updatePrompt }] }
139
- wait for terminal result
146
+ continue independent app implementation; do not stop to wait
147
+ terminal result
140
148
  sync_ai_artifacts
141
149
  read .howone/ai/manifest.json
142
150
  update SDK workflowId bindings from the newly synced manifest
143
151
  ```
144
152
 
145
- SDK/code work happens after these steps and belongs to the SDK track.
153
+ Final workflow-ID SDK binding happens after terminal sync and belongs to the SDK track. Other app
154
+ source work proceeds while the external job is pending.
146
155
 
147
156
  ## Checklist
148
157
 
@@ -1,329 +1,162 @@
1
1
  # Client Setup
2
2
 
3
- **Track:** `04-app-sdk/` implement HowOne in the app from synced manifests; not schema/AI design.
3
+ Use this track after synced manifests exist and before writing app code that imports
4
+ `@howone/sdk`. Read [version-evidence.md](../../references/version-evidence.md) first when the installed
5
+ SDK version is uncertain.
4
6
 
5
- ## createClient
7
+ ## One client, one source of truth
6
8
 
7
- `createClient(opts: CreateClientOptions)` is the single factory for everything in the HowOne SDK. Call it once at module level and export the result (or the composed `howone` client).
8
-
9
- ### CreateClientOptions — full type
10
-
11
- ```ts
12
- type Environment = 'local' | 'dev' | 'prod'
13
-
14
- type CreateClientOptions = {
15
- // ── Required ──────────────────────────────────────────────
16
- projectId?: string // Your HowOne project ID (set via VITE_HOWONE_PROJECT_ID)
17
-
18
- // ── Environment ───────────────────────────────────────────
19
- env?: Environment | string // 'local' | 'dev' | 'prod' (set via VITE_HOWONE_ENV)
20
- apiUrl?: string // Override the REST API base URL
21
- aiUrl?: string // Override the AI/SSE base URL
22
-
23
- // ── Behaviour ─────────────────────────────────────────────
24
- caseStyle?: 'camel' | 'snake' // Default: 'camel'
25
- mode?: 'auto' | 'standalone' | 'embedded'
26
-
27
- // ── Auth (one parameter for custom login) ─────────────────
28
- auth?: 'custom' | 'hosted' | 'headless' | 'none' | {
29
- mode?: 'custom' | 'hosted' | 'headless' | 'none' | 'managed'
30
- loginPath?: string // default '/login' when mode is 'custom'
31
- logoutPath?: string
32
- guard?: 'required' | 'optional' | 'none'
33
- getToken?: () => Promise<string | null>
34
- adapter?: AuthAdapter
35
- tokenCacheMs?: number
36
- }
37
- loginPath?: string // shorthand when auth is 'custom'
38
- logoutPath?: string
39
-
40
- // ── Limit-exceeded callbacks ───────────────────────────────
41
- limitExceeded?: {
42
- onLimitExceeded?: (context: LimitExceededContext) => void
43
- showUpgradeToast?: boolean
44
- upgradeUrl?: string
45
- }
46
-
47
- // ── Deprecated — do not use in new code ───────────────────
48
- appId?: string // Use projectId
49
- baseUrl?: string // Use apiUrl / aiUrl
50
- apiBaseUrl?: string // Use apiUrl
51
- aiBaseUrl?: string // Use aiUrl
52
- authRequired?: boolean // Use auth.mode
53
- }
54
- ```
55
-
56
- ### What createClient returns
57
-
58
- ```ts
59
- const client = createClient({ ... })
60
-
61
- client.projectId // string — resolved project ID
62
- client.appId // string — alias for projectId
63
- client.caseStyle // 'camel' | 'snake'
64
-
65
- // Entity factory
66
- client.entity<TRecord, TCreate, TUpdate>(entityName: string): EntityClient
67
-
68
- // Typed entity map (populated via withEntities)
69
- client.entities: Record<string, EntityClient>
70
-
71
- // Public entity namespace (never sends Authorization)
72
- client.public.entity<TRecord, TPublicCreate, TPublicUpdate>(entityName: string): PublicEntityClient
73
- client.public.entities: Record<string, PublicEntityClient>
74
- client.public.raw: RawHttpClient
75
-
76
- // Schema contract/version client
77
- client.schema.listDefinitions()
78
- client.schema.getDefinition(entityName)
79
- client.schema.operate(operation)
80
- client.schema.applyPatch(patch, { expectedVersionId, reason })
81
- client.schema.getState()
82
- client.schema.listVersions()
83
- client.schema.getVersion(versionId)
84
- client.schema.restore(versionId, reason?)
85
-
86
- // AI action runner (low-level)
87
- client.ai: AiClient
88
-
89
- // HTTP utilities (low-level — see 04-app-sdk/06-raw-http.md)
90
- client.raw: RawHttpClient
91
-
92
- // File upload
93
- client.upload.file(file, options?)
94
- client.upload.image(file)
95
- client.upload.batch(options)
96
-
97
- // User profile
98
- client.me(options?) // Promise<UserProfile | null>
99
- client.requireMe(options?) // Promise<UserProfile> throws if unauthenticated
100
- client.session.user() // alias for client.me()
101
-
102
- // Auth helpers (behavior driven by createClient auth mode)
103
- client.auth.mode // 'custom' | 'hosted' | 'headless' | 'none'
104
- client.auth.loginPath // e.g. '/login'
105
- client.auth.setToken(token: string | null)
106
- client.auth.getToken(): string | null
107
- client.auth.isAuthenticated(): boolean
108
- client.auth.login(returnUrl?: string)
109
- await client.auth.logout()
110
- await client.auth.clearSession({ redirect?: false | string })
111
- client.auth.subscribe((state) => { ... }) // auth state callback
112
-
113
- // URL utilities
114
- client.sanitizeUrl(opts?: { clearAll?: boolean; sensitiveParams?: string[] })
115
- ```
116
-
117
- ---
118
-
119
- ## Standard Vite Setup
9
+ Create the client once at module scope in `src/lib/sdk.ts`. Use the same composed instance for
10
+ entities, AI, uploads, auth, and raw requests. Do not create clients inside components or handlers.
120
11
 
121
12
  ```ts
122
13
  // src/lib/sdk.ts
123
14
  import {
124
15
  createClient,
125
- defineAiAction,
126
16
  defineAiActions,
127
17
  defineEntities,
128
- pickEntityPayload,
129
- runAiActionAndPersist,
130
- type EntityRecord,
131
18
  withAiActions,
132
19
  withEntities,
133
20
  } from '@howone/sdk'
134
- import { z } from 'zod'
135
21
 
136
- // ── 1. Create client ─────────────────────────────────────────
137
22
  const client = createClient({
138
23
  projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
139
24
  env: import.meta.env.VITE_HOWONE_ENV,
140
25
  })
141
26
 
142
- // ── 2. Define entity types & bind ────────────────────────────
143
- // (see 04-app-sdk/02-entity-operations.md for full details)
144
- export type NoteRecord = EntityRecord & { title: string; body: string }
145
- export type NoteCreate = { title: string; body: string }
146
- export type NoteUpdate = Partial<NoteCreate>
147
-
148
27
  export const entities = defineEntities({
149
- Note: client.entity<NoteRecord, NoteCreate, NoteUpdate>('Note'),
28
+ // Generated bindings go here; read 01-architect/02-manifest-codegen.md.
150
29
  })
30
+ export const ai = defineAiActions({})
151
31
 
152
- // ── 3. Define AI actions ─────────────────────────────────────
153
- // (see 04-app-sdk/07-ai-action-calls.md for full details)
154
- export const summarizeInputSchema = z.object({ noteId: z.string() })
155
- export type SummarizeInput = z.infer<typeof summarizeInputSchema>
156
-
157
- export const ai = defineAiActions({
158
- summarizeNote: defineAiAction('summarizeNote', {
159
- inputSchema: summarizeInputSchema,
160
- }),
161
- })
162
-
163
- // ── 4. Compose and export ────────────────────────────────────
164
32
  const howone = withAiActions(withEntities(client, entities), ai)
165
33
  export default howone
166
34
  ```
167
35
 
168
- SDK utility exports that generated apps may use:
36
+ `projectId` is required. Missing or invalid Vite variables must fail loudly; do not add empty
37
+ fallbacks such as `?? ''` or silently default the environment to production.
169
38
 
170
- | Utility | Use |
171
- |---|---|
172
- | `pickEntityPayload(definition, payload)` | Keep only schema-declared business fields before create/update. |
173
- | `validateEntityPayload(definition, payload)` | Return structured issues for unknown/system/ownership/missing required fields. |
174
- | `assertEntityPayload(definition, payload)` | Throw structured `EntityPayloadValidationError` before unsafe writes. |
175
- | `validatePublicEntityQuery(definition, options)` | Check public filters, sorts, scopes, and limits against `access.public`. |
176
- | `assertPublicEntityQuery(definition, options)` | Throw before generating an invalid public query. |
177
- | `runAiActionAndPersist(options)` | Standard pending-first AI execution + entity persistence helper. |
39
+ ## Environment-owned routing
178
40
 
179
- ---
41
+ `env` selects the complete deployment target. The REST API, auth endpoints, uploads, and EAX
42
+ workflow endpoint must use the same environment. EAX URLs are intentionally not configurable per
43
+ request or through `createClient`:
180
44
 
181
- ## Environment Variables
45
+ | `env` | REST base | EAX workflow base |
46
+ |---|---|---|
47
+ | `local` | `http://localhost:3002/api` | `https://eax-backend-orchestrator-dev.fly.dev` |
48
+ | `dev` | `https://api.howone.dev/api` | `https://eax-backend-orchestrator-dev.fly.dev` |
49
+ | `prod` | `https://api.howone.ai/api` | `https://eax-backend-orchestrator-prod.fly.dev` |
182
50
 
183
- In Vite apps, these two env vars are mandatory:
51
+ Never add `aiUrl`, `aiBaseUrl`, or an EAX URL environment variable. If a special deployment needs a
52
+ different REST API, `apiUrl` is the only supported URL override; it does not change EAX routing.
184
53
 
185
- ```
186
- VITE_HOWONE_PROJECT_ID=proj_xxxxxxxxxxxxxxxx
187
- VITE_HOWONE_ENV=prod
54
+ The selected values are inspectable without guessing:
55
+
56
+ ```ts
57
+ howone.runtime.environment // 'local' | 'dev' | 'prod'
58
+ howone.runtime.apiBaseUrl
59
+ howone.runtime.aiBaseUrl
60
+ howone.runtime.authRoot
61
+ howone.runtime.authCookieRoot
188
62
  ```
189
63
 
190
- Rules:
191
- - **Do not** add `?? 'prod'` or `?? ''` fallbacks. Missing env vars should surface as misconfiguration errors.
192
- - **Do not** hardcode project IDs in source. Use the env var.
193
- - `env` accepts `'local'`, `'dev'`, or `'prod'`. **Auth OTP/OAuth, entities, AI, and uploads all use this same env.**
194
- - Import `src/lib/sdk.ts` before calling `loginWithEmailCode` / `unifiedAuth` so env is pinned (otherwise auth defaults to prod APIs).
64
+ `howone.runtime` is immutable. A second client created later cannot retarget an existing client's
65
+ requests. Prefer one client per app; if tests create several, assert each client's runtime.
195
66
 
196
- | `env` | API base | Auth API example |
197
- |-------|----------|------------------|
198
- | `local` | `http://localhost:3002/api` | `http://localhost:3002/api/auth/email/send-code` |
199
- | `dev` | `https://api.howone.dev/api` | `https://api.howone.dev/api/auth/email/send-code` |
200
- | `prod` | `https://api.howone.ai/api` | `https://api.howone.ai/api/auth/email/send-code` |
67
+ ## Options that matter
201
68
 
202
- ---
69
+ ```ts
70
+ type CreateClientOptions = {
71
+ projectId?: string
72
+ env?: 'local' | 'dev' | 'prod' | string
73
+ apiUrl?: string // REST-only exceptional override
74
+ apiBaseUrl?: string // deprecated alias for apiUrl
75
+ appId?: string // deprecated alias for projectId
76
+ caseStyle?: 'camel' | 'snake' // default 'camel'
77
+ caseDepth?: 'top-level' | 'deep' // default 'top-level'
78
+ auth?: 'hosted' | 'custom' | 'headless' | 'none' | {
79
+ mode?: 'hosted' | 'custom' | 'headless' | 'none'
80
+ guard?: 'required' | 'optional' | 'none'
81
+ loginPath?: string
82
+ logoutPath?: string
83
+ getToken?: () => Promise<string | null>
84
+ adapter?: AuthAdapter
85
+ tokenCacheMs?: number
86
+ }
87
+ loginPath?: string
88
+ logoutPath?: string
89
+ authRequired?: boolean // deprecated compatibility alias
90
+ limitExceeded?: LimitExceededHandlerOptions
91
+ }
92
+ ```
203
93
 
204
- ## Auth Modes
94
+ Do not use the removed `mode: 'auto' | 'standalone' | 'embedded'` option. That was not a runtime
95
+ behavior switch. Authentication mode belongs under `auth.mode` and means hosted/custom/headless/none.
205
96
 
206
- See `04-app-sdk/03-auth.md` for the full custom-login playbook.
97
+ ## Authentication posture
207
98
 
208
99
  ```ts
209
- // Default HowOne hosted login (howone.dev / howone.ai)
100
+ // Hosted login (default): Provider inherits a required guard.
210
101
  createClient({ projectId, env })
211
102
 
212
- // Custom in-app login page; auth APIs still HowOne
103
+ // App-owned login page using HowOne auth APIs.
213
104
  createClient({ projectId, env, auth: 'custom', loginPath: '/login' })
214
105
 
215
- // Headless external JWT provider
106
+ // External identity provider; adapter supplies a token.
216
107
  createClient({
217
108
  projectId,
218
109
  env,
219
110
  auth: {
220
111
  mode: 'headless',
221
112
  adapter: {
222
- getToken: async () => externalAuth.getToken(),
113
+ getToken: () => externalAuth.getToken(),
223
114
  setToken: (token) => externalAuth.setToken(token),
224
- login: ({ returnUrl } = {}) => router.push(`/login?redirect=${encodeURIComponent(returnUrl ?? '/')}`),
225
- logout: () => router.push('/'),
226
115
  },
227
- tokenCacheMs: 60_000,
228
116
  },
229
117
  })
230
118
 
231
- // None public app, no auth
119
+ // Deliberately public app.
232
120
  createClient({ projectId, env, auth: 'none' })
233
121
  ```
234
122
 
235
- ---
123
+ `HowOneProvider` receives `client={howone}` and derives its default route guard from
124
+ `client.auth.guard`. Override `auth="required|optional|none"` only when the UI intentionally needs
125
+ a different guard. Do not configure auth once on the client and independently guess it on the
126
+ Provider.
236
127
 
237
- ## Multi-environment Setup
128
+ ## Compose manifest bindings
238
129
 
239
- ```ts
240
- // src/lib/sdk.ts
241
- const client = createClient({
242
- projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
243
- env: import.meta.env.VITE_HOWONE_ENV,
244
- // Override URLs only for special deployments
245
- // apiUrl: import.meta.env.VITE_HOWONE_API_URL,
246
- // aiUrl: import.meta.env.VITE_HOWONE_AI_URL,
247
- })
248
- ```
249
-
250
- ---
251
-
252
- ## UserProfile Type
130
+ Use contract wrappers for generated entities and actions. Public bindings are a separate namespace;
131
+ never call an authenticated entity client from a public page just because the entity name matches.
253
132
 
254
133
  ```ts
255
- type UserProfile = {
256
- id: string
257
- userId?: string
258
- puid?: string
259
- email?: string
260
- name?: string
261
- avatarUrl?: string
262
- appId?: string
263
- roles?: string[]
264
- metadata?: Record<string, unknown>
265
- }
266
-
267
- // Usage
268
- const me = await client.me() // null if not logged in
269
- const me = await client.requireMe() // throws HowOneAuthError if not logged in
270
-
271
- // Check auth state programmatically
272
- const isLoggedIn = client.auth.isAuthenticated()
273
- const token = client.auth.getToken()
274
-
275
- // Manually set a token (e.g. after custom login flow)
276
- client.auth.setToken(jwtToken)
277
-
278
- // Trigger login redirect
279
- client.auth.login('/dashboard') // optional return path
280
-
281
- // Logout
282
- client.auth.logout()
134
+ const howone = withAiActions(
135
+ withPublicEntities(
136
+ withEntities(client, entities),
137
+ publicEntities,
138
+ ),
139
+ ai,
140
+ )
283
141
  ```
284
142
 
285
- ---
286
-
287
- ## Client Namespace Rules
288
-
289
- - Use `client.entities.*` / `howone.entities.*` for authenticated app data. The backend derives owner from the JWT; do not pass owner filters or owner fields.
290
- - Use `client.public.entities.*` / `howone.public.entities.*` for public landing pages, public article lists, scoped QR/profile pages, and public forms.
291
- - Use `client.schema.*` only for backend contract management: definitions, schema operations, schema patch apply, versions, and restore.
292
- - Use `client.raw.*` only for custom endpoints not covered by typed SDK methods.
293
-
294
- ---
295
-
296
- ## HowOneAuthError
143
+ The exact generated composition belongs in `01-architect/02-manifest-codegen.md`.
297
144
 
298
- ```ts
299
- import { HowOneAuthError } from '@howone/sdk'
300
-
301
- try {
302
- const user = await client.requireMe()
303
- } catch (err) {
304
- if (err instanceof HowOneAuthError) {
305
- // err.code === 'UNAUTHENTICATED'
306
- client.auth.login()
307
- }
308
- }
309
- ```
145
+ ## Runtime activation checklist
310
146
 
311
- ---
147
+ - Keep `src/lib/sdk.ts` imported before rendering `HowOneProvider`.
148
+ - Add a Provider whenever app code uses entities, AI, uploads, auth, session, or raw SDK requests.
149
+ - Use `auth: 'none'` only for a deliberately public app or when the app owns its route guard.
150
+ - Use real token acquisition for authenticated AI/entity/upload calls; do not substitute mock data.
151
+ - Run `node .howone/skills/howone/scripts/verify-project.mjs` before typecheck/build.
312
152
 
313
- ## LimitExceeded Handling
153
+ ## Common mistakes
314
154
 
315
- ```ts
316
- const client = createClient({
317
- projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
318
- env: import.meta.env.VITE_HOWONE_ENV,
319
- limitExceeded: {
320
- showUpgradeToast: true,
321
- upgradeUrl: 'https://howone.app/upgrade',
322
- onLimitExceeded: (context) => {
323
- console.error('Limit exceeded:', context.source, context.message)
324
- // context.source: 'axios-response' | 'workflow-executor-sse' | ...
325
- // context.status: HTTP status code (if available)
326
- },
327
- },
328
- })
329
- ```
155
+ | Mistake | Correct pattern |
156
+ |---|---|
157
+ | AI URL in `.env` or `createClient` | Set only `env`; EAX is environment-owned. |
158
+ | `HowOneProvider` without the composed client | `<HowOneProvider client={howone}>`. |
159
+ | Client created in a component | Module-level singleton in `src/lib/sdk.ts`. |
160
+ | Using `apiUrl` to point at EAX | `apiUrl` is REST-only; EAX remains the env mapping. |
161
+ | Passing a JSON Schema object to an AI action | Generate a Zod schema first. |
162
+ | Calling `client.entities.Unknown` after strict codegen | Use `client.entity<T...>('Unknown')` only for an explicit, reviewed escape hatch. |