ts-procedures 5.2.0 → 5.4.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 (49) hide show
  1. package/README.md +150 -0
  2. package/agent_config/bin/postinstall.mjs +105 -0
  3. package/agent_config/bin/setup.mjs +286 -0
  4. package/agent_config/claude-code/.claude-plugin/plugin.json +5 -0
  5. package/agent_config/claude-code/agents/ts-procedures-architect.md +188 -0
  6. package/agent_config/claude-code/skills/guide/SKILL.md +142 -0
  7. package/agent_config/claude-code/skills/guide/anti-patterns.md +608 -0
  8. package/agent_config/claude-code/skills/guide/api-reference.md +696 -0
  9. package/agent_config/claude-code/skills/guide/patterns.md +727 -0
  10. package/agent_config/claude-code/skills/review/SKILL.md +53 -0
  11. package/agent_config/claude-code/skills/review/checklist.md +163 -0
  12. package/agent_config/claude-code/skills/scaffold/SKILL.md +56 -0
  13. package/agent_config/claude-code/skills/scaffold/templates/express-rpc.md +134 -0
  14. package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +169 -0
  15. package/agent_config/claude-code/skills/scaffold/templates/hono-rpc.md +139 -0
  16. package/agent_config/claude-code/skills/scaffold/templates/hono-stream.md +134 -0
  17. package/agent_config/claude-code/skills/scaffold/templates/procedure.md +77 -0
  18. package/agent_config/claude-code/skills/scaffold/templates/stream-procedure.md +113 -0
  19. package/agent_config/copilot/copilot-instructions.md +290 -0
  20. package/agent_config/cursor/cursorrules +290 -0
  21. package/agent_config/lib/install-claude.mjs +109 -0
  22. package/build/implementations/http/hono-api/index.d.ts +102 -0
  23. package/build/implementations/http/hono-api/index.js +339 -0
  24. package/build/implementations/http/hono-api/index.js.map +1 -0
  25. package/build/implementations/http/hono-api/index.test.d.ts +1 -0
  26. package/build/implementations/http/hono-api/index.test.js +983 -0
  27. package/build/implementations/http/hono-api/index.test.js.map +1 -0
  28. package/build/implementations/http/hono-api/types.d.ts +13 -0
  29. package/build/implementations/http/hono-api/types.js +2 -0
  30. package/build/implementations/http/hono-api/types.js.map +1 -0
  31. package/build/implementations/types.d.ts +44 -0
  32. package/build/index.d.ts +28 -6
  33. package/build/index.js +28 -0
  34. package/build/index.js.map +1 -1
  35. package/build/schema/compute-schema.d.ts +5 -0
  36. package/build/schema/compute-schema.js +8 -1
  37. package/build/schema/compute-schema.js.map +1 -1
  38. package/build/schema/parser.d.ts +6 -5
  39. package/build/schema/parser.js +54 -0
  40. package/build/schema/parser.js.map +1 -1
  41. package/package.json +14 -4
  42. package/src/implementations/http/README.md +45 -2
  43. package/src/implementations/http/hono-api/index.test.ts +1328 -0
  44. package/src/implementations/http/hono-api/index.ts +461 -0
  45. package/src/implementations/http/hono-api/types.ts +16 -0
  46. package/src/implementations/types.ts +52 -0
  47. package/src/index.ts +87 -10
  48. package/src/schema/compute-schema.ts +23 -2
  49. package/src/schema/parser.ts +70 -3
@@ -0,0 +1,290 @@
1
+ # ts-procedures Framework Reference
2
+
3
+ You are assisting a developer using **ts-procedures**, a TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition.
4
+
5
+ ## Core Flow
6
+
7
+ ```
8
+ Procedures<TContext, TExtendedConfig>(builder?)
9
+
10
+ Create(name, config, handler) → Standard async procedure
11
+ CreateStream(name, config, handler) → Streaming async generator
12
+
13
+ Returns: { [name]: handler, procedure: handler, info: metadata }
14
+ ```
15
+
16
+ ## Imports
17
+
18
+ ```typescript
19
+ // Core
20
+ import { Procedures } from 'ts-procedures'
21
+ import type { TLocalContext, TStreamContext, TProcedureRegistration, TStreamProcedureRegistration } from 'ts-procedures'
22
+
23
+ // Errors
24
+ import { ProcedureError, ProcedureValidationError, ProcedureYieldValidationError, ProcedureRegistrationError } from 'ts-procedures'
25
+
26
+ // HTTP types
27
+ import type { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode } from 'ts-procedures/http'
28
+
29
+ // Express RPC
30
+ import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
31
+
32
+ // Hono RPC
33
+ import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
34
+
35
+ // Hono Streaming
36
+ import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
37
+
38
+ // Hono API (REST-style)
39
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
40
+ import type { APIConfig, APIInput } from 'ts-procedures/hono-api'
41
+ ```
42
+
43
+ ## Architecture Rules
44
+
45
+ 1. **schema.params is validated at runtime; schema.returnType is documentation only.** Never expect returnType to be validated. Use TypeScript for return type safety.
46
+
47
+ 2. **Use ctx.error() for business logic errors.** Never throw raw `Error` instances. `ctx.error(message, meta?)` creates `ProcedureError` with procedure name, metadata, and enhanced stack trace.
48
+
49
+ 3. **Pass ctx.signal to all downstream async calls.** HTTP implementations inject AbortSignal automatically. Pass it to fetch, database queries, and other async operations for cancellation support.
50
+
51
+ 4. **Stream handlers always have ctx.signal (guaranteed AbortSignal).** Standard handlers get it when HTTP implementations provide it. Check `signal.reason === 'stream-completed'` to distinguish normal completion from client disconnect.
52
+
53
+ 5. **Use TypeBox for schemas** (`import { Type } from 'typebox'`). Plain JSON Schema objects are not recognized. AJV config: `allErrors: true`, `coerceTypes: true`, `removeAdditional: true`.
54
+
55
+ 6. **One factory per access level.** Separate `Procedures()` factories for public, authenticated, admin contexts.
56
+
57
+ 7. **onCreate callback enables framework integration.** Use for route registration, OpenAPI generation, logging.
58
+
59
+ 8. **getProcedures() for introspection.** Returns all registered procedures with metadata.
60
+
61
+ ## Procedure Pattern
62
+
63
+ ```typescript
64
+ import { Procedures } from 'ts-procedures'
65
+ import { Type } from 'typebox'
66
+
67
+ type AppContext = { userId: string; signal?: AbortSignal }
68
+
69
+ const { Create, CreateStream } = Procedures<AppContext>()
70
+
71
+ // Standard procedure
72
+ const { GetUser } = Create(
73
+ 'GetUser',
74
+ {
75
+ description: 'Fetch user by ID',
76
+ schema: {
77
+ params: Type.Object({ id: Type.String() }),
78
+ returnType: Type.Object({ id: Type.String(), name: Type.String() }),
79
+ },
80
+ },
81
+ async (ctx, params) => {
82
+ // params.id guaranteed string (AJV validated)
83
+ const user = await fetchUser(params.id, { signal: ctx.signal })
84
+ if (!user) throw ctx.error('Not found', { code: 'NOT_FOUND' })
85
+ return user
86
+ }
87
+ )
88
+ ```
89
+
90
+ ## Stream Procedure Pattern
91
+
92
+ ```typescript
93
+ const { StreamEvents } = CreateStream(
94
+ 'StreamEvents',
95
+ {
96
+ schema: {
97
+ params: Type.Object({ channel: Type.String() }),
98
+ yieldType: Type.Object({ type: Type.String(), data: Type.Any() }),
99
+ },
100
+ },
101
+ async function* (ctx, params) {
102
+ // ctx.signal always present in streams
103
+ while (!ctx.signal.aborted) {
104
+ const event = await pollEvents(params.channel, { signal: ctx.signal })
105
+ yield event
106
+ }
107
+ }
108
+ )
109
+ ```
110
+
111
+ ## Express RPC Pattern
112
+
113
+ ```typescript
114
+ import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
115
+ import type { RPCConfig } from 'ts-procedures/http'
116
+
117
+ const RPC = Procedures<AppContext, RPCConfig>()
118
+
119
+ RPC.Create('GetUser', {
120
+ scope: 'users', version: 1,
121
+ schema: { params: Type.Object({ id: Type.String() }) },
122
+ }, async (ctx, params) => fetchUser(params.id))
123
+
124
+ const app = new ExpressRPCAppBuilder({ pathPrefix: '/api' })
125
+ .register(RPC, async (req) => ({ userId: await authenticate(req) }))
126
+ .build()
127
+ // POST /api/users/get-user/1
128
+ ```
129
+
130
+ ## Hono RPC Pattern
131
+
132
+ ```typescript
133
+ import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
134
+
135
+ const app = new HonoRPCAppBuilder({ pathPrefix: '/api' })
136
+ .register(RPC, (c) => ({ userId: c.req.header('x-user-id') }))
137
+ .build()
138
+ ```
139
+
140
+ ## Hono Streaming Pattern
141
+
142
+ ```typescript
143
+ import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
144
+
145
+ const StreamRPC = Procedures<AppContext, RPCConfig>()
146
+
147
+ StreamRPC.CreateStream('Feed', {
148
+ scope: 'events', version: 1,
149
+ schema: { params: Type.Object({ channel: Type.String() }) },
150
+ }, async function* (ctx, params) {
151
+ while (!ctx.signal.aborted) {
152
+ const event = await poll({ signal: ctx.signal })
153
+ yield sse(event, { event: 'update' })
154
+ }
155
+ })
156
+
157
+ const app = new HonoStreamAppBuilder({ defaultStreamMode: 'sse' })
158
+ .register(StreamRPC, (c) => ({ userId: c.req.header('x-user-id') }))
159
+ .build()
160
+ // GET|POST /events/feed/1
161
+ ```
162
+
163
+ ## Hono API Pattern (REST-style)
164
+
165
+ ```typescript
166
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
167
+ import type { APIConfig } from 'ts-procedures/http'
168
+
169
+ const API = Procedures<AppContext, APIConfig>()
170
+
171
+ API.Create('GetUser', {
172
+ path: '/users/:id', method: 'get',
173
+ schema: {
174
+ input: { pathParams: Type.Object({ id: Type.String() }) },
175
+ },
176
+ }, async (ctx, { pathParams }) => fetchUser(pathParams.id))
177
+
178
+ API.Create('CreateUser', {
179
+ path: '/users', method: 'post',
180
+ schema: {
181
+ input: { body: Type.Object({ name: Type.String() }) },
182
+ },
183
+ }, async (ctx, { body }) => createUser(body))
184
+
185
+ const app = await new HonoAPIAppBuilder({ pathPrefix: '/api' })
186
+ .register(API, (c) => ({ userId: c.req.header('x-user-id') }))
187
+ .build()
188
+ // GET /api/users/:id → 200, POST /api/users → 201
189
+ ```
190
+
191
+ ## Error Handling
192
+
193
+ | Error Class | Trigger | HTTP Status |
194
+ |-------------|---------|-------------|
195
+ | `ProcedureValidationError` | Schema params validation failure | 400 |
196
+ | `ProcedureError` | `ctx.error()` or unhandled handler exception | 422/500 |
197
+ | `ProcedureYieldValidationError` | Yield validation failure (validateYields: true) | N/A (mid-stream) |
198
+ | `ProcedureRegistrationError` | Invalid schema or duplicate name at registration | N/A (startup) |
199
+
200
+ ```typescript
201
+ // In HTTP builder
202
+ onError: (procedure, req, res, error) => {
203
+ if (error instanceof ProcedureValidationError) {
204
+ res.status(400).json({ error: error.message, details: error.errors })
205
+ } else if (error instanceof ProcedureError) {
206
+ res.status(422).json({ error: error.message, meta: error.meta })
207
+ } else {
208
+ res.status(500).json({ error: 'Internal server error' })
209
+ }
210
+ }
211
+ ```
212
+
213
+ ## Lifecycle Hook Order
214
+
215
+ ### Standard RPC
216
+ ```
217
+ onRequestStart → factoryContext() → handler() → onSuccess → onRequestEnd
218
+ → onError → onRequestEnd
219
+ ```
220
+
221
+ ### Streaming
222
+ ```
223
+ onRequestStart → factoryContext() → validation → onStreamStart → handler yields → onStreamEnd → onRequestEnd
224
+ → onPreStreamError → onRequestEnd
225
+ → onMidStreamError → onStreamEnd → onRequestEnd
226
+ ```
227
+
228
+ ## Decision Framework
229
+
230
+ **Which procedure type?**
231
+ - Single response → `Create`
232
+ - Multiple values over time → `CreateStream`
233
+
234
+ **Which schema library?**
235
+ - **TypeBox** (`import { Type } from 'typebox'`)
236
+
237
+ **Which HTTP implementation?**
238
+ - Express → `ExpressRPCAppBuilder`
239
+ - Hono (standard) → `HonoRPCAppBuilder`
240
+ - Hono (streaming) → `HonoStreamAppBuilder`
241
+ - Hono (REST-style, per-channel input) → `HonoAPIAppBuilder`
242
+
243
+ **Stream mode?**
244
+ - Browser EventSource → `'sse'` (default)
245
+ - Simple HTTP client → `'text'` (newline-delimited JSON)
246
+
247
+ ## Anti-Patterns — NEVER Do These
248
+
249
+ 1. **Never throw raw Error** — use `ctx.error(message, meta?)`
250
+ 2. **Never expect returnType runtime validation** — it's docs only
251
+ 3. **Never put validation logic in handler** — use `schema.params`
252
+ 4. **Never ignore ctx.signal** — pass to all async calls
253
+ 5. **Never skip signal.aborted check in stream loops** — causes resource leaks
254
+ 6. **Never register Create procedures with HonoStreamAppBuilder** — they're silently ignored
255
+ 7. **Never use plain JSON Schema objects** — use TypeBox builders
256
+ 8. **Never swallow errors without re-throwing** — hides failures
257
+ 9. **Never assume extra params fields survive** — `removeAdditional: true` strips them
258
+ 10. **Never manually parse types AJV coerces** — `coerceTypes: true` handles it
259
+ 11. **Never define both schema.params and schema.input** — mutually exclusive, throws ProcedureRegistrationError
260
+ 12. **Never forget to await HonoAPIAppBuilder.build()** — it's async (resolves query parser)
261
+
262
+ ## Testing
263
+
264
+ ```typescript
265
+ import { describe, test, expect } from 'vitest'
266
+ import { ProcedureError, ProcedureValidationError } from 'ts-procedures'
267
+
268
+ describe('GetUser', () => {
269
+ test('valid params', async () => {
270
+ const result = await GetUser(mockCtx, { id: 'user-1' })
271
+ expect(result.id).toBe('user-1')
272
+ })
273
+
274
+ test('invalid params', async () => {
275
+ await expect(GetUser(mockCtx, {})).rejects.toThrow(ProcedureValidationError)
276
+ })
277
+
278
+ test('business error', async () => {
279
+ await expect(GetUser(mockCtx, { id: 'missing' })).rejects.toThrow(ProcedureError)
280
+ })
281
+ })
282
+ ```
283
+
284
+ ## HTTP Route Path Format
285
+
286
+ `{pathPrefix}/{scope}/{kebab-case-name}/{version}`
287
+
288
+ - `scope` can be string or string[] (joined as path segments)
289
+ - Procedure name auto-converted to kebab-case
290
+ - Stream routes support both GET (query params) and POST (JSON body)
@@ -0,0 +1,290 @@
1
+ # ts-procedures Framework Reference
2
+
3
+ You are assisting a developer using **ts-procedures**, a TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition.
4
+
5
+ ## Core Flow
6
+
7
+ ```
8
+ Procedures<TContext, TExtendedConfig>(builder?)
9
+
10
+ Create(name, config, handler) → Standard async procedure
11
+ CreateStream(name, config, handler) → Streaming async generator
12
+
13
+ Returns: { [name]: handler, procedure: handler, info: metadata }
14
+ ```
15
+
16
+ ## Imports
17
+
18
+ ```typescript
19
+ // Core
20
+ import { Procedures } from 'ts-procedures'
21
+ import type { TLocalContext, TStreamContext, TProcedureRegistration, TStreamProcedureRegistration } from 'ts-procedures'
22
+
23
+ // Errors
24
+ import { ProcedureError, ProcedureValidationError, ProcedureYieldValidationError, ProcedureRegistrationError } from 'ts-procedures'
25
+
26
+ // HTTP types
27
+ import type { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode } from 'ts-procedures/http'
28
+
29
+ // Express RPC
30
+ import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
31
+
32
+ // Hono RPC
33
+ import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
34
+
35
+ // Hono Streaming
36
+ import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
37
+
38
+ // Hono API (REST-style)
39
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
40
+ import type { APIConfig, APIInput } from 'ts-procedures/hono-api'
41
+ ```
42
+
43
+ ## Architecture Rules
44
+
45
+ 1. **schema.params is validated at runtime; schema.returnType is documentation only.** Never expect returnType to be validated. Use TypeScript for return type safety.
46
+
47
+ 2. **Use ctx.error() for business logic errors.** Never throw raw `Error` instances. `ctx.error(message, meta?)` creates `ProcedureError` with procedure name, metadata, and enhanced stack trace.
48
+
49
+ 3. **Pass ctx.signal to all downstream async calls.** HTTP implementations inject AbortSignal automatically. Pass it to fetch, database queries, and other async operations for cancellation support.
50
+
51
+ 4. **Stream handlers always have ctx.signal (guaranteed AbortSignal).** Standard handlers get it when HTTP implementations provide it. Check `signal.reason === 'stream-completed'` to distinguish normal completion from client disconnect.
52
+
53
+ 5. **Use TypeBox for schemas** (`import { Type } from 'typebox'`). Plain JSON Schema objects are not recognized. AJV config: `allErrors: true`, `coerceTypes: true`, `removeAdditional: true`.
54
+
55
+ 6. **One factory per access level.** Separate `Procedures()` factories for public, authenticated, admin contexts.
56
+
57
+ 7. **onCreate callback enables framework integration.** Use for route registration, OpenAPI generation, logging.
58
+
59
+ 8. **getProcedures() for introspection.** Returns all registered procedures with metadata.
60
+
61
+ ## Procedure Pattern
62
+
63
+ ```typescript
64
+ import { Procedures } from 'ts-procedures'
65
+ import { Type } from 'typebox'
66
+
67
+ type AppContext = { userId: string; signal?: AbortSignal }
68
+
69
+ const { Create, CreateStream } = Procedures<AppContext>()
70
+
71
+ // Standard procedure
72
+ const { GetUser } = Create(
73
+ 'GetUser',
74
+ {
75
+ description: 'Fetch user by ID',
76
+ schema: {
77
+ params: Type.Object({ id: Type.String() }),
78
+ returnType: Type.Object({ id: Type.String(), name: Type.String() }),
79
+ },
80
+ },
81
+ async (ctx, params) => {
82
+ // params.id guaranteed string (AJV validated)
83
+ const user = await fetchUser(params.id, { signal: ctx.signal })
84
+ if (!user) throw ctx.error('Not found', { code: 'NOT_FOUND' })
85
+ return user
86
+ }
87
+ )
88
+ ```
89
+
90
+ ## Stream Procedure Pattern
91
+
92
+ ```typescript
93
+ const { StreamEvents } = CreateStream(
94
+ 'StreamEvents',
95
+ {
96
+ schema: {
97
+ params: Type.Object({ channel: Type.String() }),
98
+ yieldType: Type.Object({ type: Type.String(), data: Type.Any() }),
99
+ },
100
+ },
101
+ async function* (ctx, params) {
102
+ // ctx.signal always present in streams
103
+ while (!ctx.signal.aborted) {
104
+ const event = await pollEvents(params.channel, { signal: ctx.signal })
105
+ yield event
106
+ }
107
+ }
108
+ )
109
+ ```
110
+
111
+ ## Express RPC Pattern
112
+
113
+ ```typescript
114
+ import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
115
+ import type { RPCConfig } from 'ts-procedures/http'
116
+
117
+ const RPC = Procedures<AppContext, RPCConfig>()
118
+
119
+ RPC.Create('GetUser', {
120
+ scope: 'users', version: 1,
121
+ schema: { params: Type.Object({ id: Type.String() }) },
122
+ }, async (ctx, params) => fetchUser(params.id))
123
+
124
+ const app = new ExpressRPCAppBuilder({ pathPrefix: '/api' })
125
+ .register(RPC, async (req) => ({ userId: await authenticate(req) }))
126
+ .build()
127
+ // POST /api/users/get-user/1
128
+ ```
129
+
130
+ ## Hono RPC Pattern
131
+
132
+ ```typescript
133
+ import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
134
+
135
+ const app = new HonoRPCAppBuilder({ pathPrefix: '/api' })
136
+ .register(RPC, (c) => ({ userId: c.req.header('x-user-id') }))
137
+ .build()
138
+ ```
139
+
140
+ ## Hono Streaming Pattern
141
+
142
+ ```typescript
143
+ import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
144
+
145
+ const StreamRPC = Procedures<AppContext, RPCConfig>()
146
+
147
+ StreamRPC.CreateStream('Feed', {
148
+ scope: 'events', version: 1,
149
+ schema: { params: Type.Object({ channel: Type.String() }) },
150
+ }, async function* (ctx, params) {
151
+ while (!ctx.signal.aborted) {
152
+ const event = await poll({ signal: ctx.signal })
153
+ yield sse(event, { event: 'update' })
154
+ }
155
+ })
156
+
157
+ const app = new HonoStreamAppBuilder({ defaultStreamMode: 'sse' })
158
+ .register(StreamRPC, (c) => ({ userId: c.req.header('x-user-id') }))
159
+ .build()
160
+ // GET|POST /events/feed/1
161
+ ```
162
+
163
+ ## Hono API Pattern (REST-style)
164
+
165
+ ```typescript
166
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
167
+ import type { APIConfig } from 'ts-procedures/http'
168
+
169
+ const API = Procedures<AppContext, APIConfig>()
170
+
171
+ API.Create('GetUser', {
172
+ path: '/users/:id', method: 'get',
173
+ schema: {
174
+ input: { pathParams: Type.Object({ id: Type.String() }) },
175
+ },
176
+ }, async (ctx, { pathParams }) => fetchUser(pathParams.id))
177
+
178
+ API.Create('CreateUser', {
179
+ path: '/users', method: 'post',
180
+ schema: {
181
+ input: { body: Type.Object({ name: Type.String() }) },
182
+ },
183
+ }, async (ctx, { body }) => createUser(body))
184
+
185
+ const app = await new HonoAPIAppBuilder({ pathPrefix: '/api' })
186
+ .register(API, (c) => ({ userId: c.req.header('x-user-id') }))
187
+ .build()
188
+ // GET /api/users/:id → 200, POST /api/users → 201
189
+ ```
190
+
191
+ ## Error Handling
192
+
193
+ | Error Class | Trigger | HTTP Status |
194
+ |-------------|---------|-------------|
195
+ | `ProcedureValidationError` | Schema params validation failure | 400 |
196
+ | `ProcedureError` | `ctx.error()` or unhandled handler exception | 422/500 |
197
+ | `ProcedureYieldValidationError` | Yield validation failure (validateYields: true) | N/A (mid-stream) |
198
+ | `ProcedureRegistrationError` | Invalid schema or duplicate name at registration | N/A (startup) |
199
+
200
+ ```typescript
201
+ // In HTTP builder
202
+ onError: (procedure, req, res, error) => {
203
+ if (error instanceof ProcedureValidationError) {
204
+ res.status(400).json({ error: error.message, details: error.errors })
205
+ } else if (error instanceof ProcedureError) {
206
+ res.status(422).json({ error: error.message, meta: error.meta })
207
+ } else {
208
+ res.status(500).json({ error: 'Internal server error' })
209
+ }
210
+ }
211
+ ```
212
+
213
+ ## Lifecycle Hook Order
214
+
215
+ ### Standard RPC
216
+ ```
217
+ onRequestStart → factoryContext() → handler() → onSuccess → onRequestEnd
218
+ → onError → onRequestEnd
219
+ ```
220
+
221
+ ### Streaming
222
+ ```
223
+ onRequestStart → factoryContext() → validation → onStreamStart → handler yields → onStreamEnd → onRequestEnd
224
+ → onPreStreamError → onRequestEnd
225
+ → onMidStreamError → onStreamEnd → onRequestEnd
226
+ ```
227
+
228
+ ## Decision Framework
229
+
230
+ **Which procedure type?**
231
+ - Single response → `Create`
232
+ - Multiple values over time → `CreateStream`
233
+
234
+ **Which schema library?**
235
+ - **TypeBox** (`import { Type } from 'typebox'`)
236
+
237
+ **Which HTTP implementation?**
238
+ - Express → `ExpressRPCAppBuilder`
239
+ - Hono (standard) → `HonoRPCAppBuilder`
240
+ - Hono (streaming) → `HonoStreamAppBuilder`
241
+ - Hono (REST-style, per-channel input) → `HonoAPIAppBuilder`
242
+
243
+ **Stream mode?**
244
+ - Browser EventSource → `'sse'` (default)
245
+ - Simple HTTP client → `'text'` (newline-delimited JSON)
246
+
247
+ ## Anti-Patterns — NEVER Do These
248
+
249
+ 1. **Never throw raw Error** — use `ctx.error(message, meta?)`
250
+ 2. **Never expect returnType runtime validation** — it's docs only
251
+ 3. **Never put validation logic in handler** — use `schema.params`
252
+ 4. **Never ignore ctx.signal** — pass to all async calls
253
+ 5. **Never skip signal.aborted check in stream loops** — causes resource leaks
254
+ 6. **Never register Create procedures with HonoStreamAppBuilder** — they're silently ignored
255
+ 7. **Never use plain JSON Schema objects** — use TypeBox builders
256
+ 8. **Never swallow errors without re-throwing** — hides failures
257
+ 9. **Never assume extra params fields survive** — `removeAdditional: true` strips them
258
+ 10. **Never manually parse types AJV coerces** — `coerceTypes: true` handles it
259
+ 11. **Never define both schema.params and schema.input** — mutually exclusive, throws ProcedureRegistrationError
260
+ 12. **Never forget to await HonoAPIAppBuilder.build()** — it's async (resolves query parser)
261
+
262
+ ## Testing
263
+
264
+ ```typescript
265
+ import { describe, test, expect } from 'vitest'
266
+ import { ProcedureError, ProcedureValidationError } from 'ts-procedures'
267
+
268
+ describe('GetUser', () => {
269
+ test('valid params', async () => {
270
+ const result = await GetUser(mockCtx, { id: 'user-1' })
271
+ expect(result.id).toBe('user-1')
272
+ })
273
+
274
+ test('invalid params', async () => {
275
+ await expect(GetUser(mockCtx, {})).rejects.toThrow(ProcedureValidationError)
276
+ })
277
+
278
+ test('business error', async () => {
279
+ await expect(GetUser(mockCtx, { id: 'missing' })).rejects.toThrow(ProcedureError)
280
+ })
281
+ })
282
+ ```
283
+
284
+ ## HTTP Route Path Format
285
+
286
+ `{pathPrefix}/{scope}/{kebab-case-name}/{version}`
287
+
288
+ - `scope` can be string or string[] (joined as path segments)
289
+ - Procedure name auto-converted to kebab-case
290
+ - Stream routes support both GET (query params) and POST (JSON body)
@@ -0,0 +1,109 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ const SKILLS_DIR = join(__dirname, '..', 'claude-code', 'skills');
8
+ const AGENTS_DIR = join(__dirname, '..', 'claude-code', 'agents');
9
+
10
+ function getPackageVersion() {
11
+ try {
12
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
13
+ return pkg.version || 'unknown';
14
+ } catch {
15
+ return 'unknown';
16
+ }
17
+ }
18
+
19
+ function makeAutoHeader() {
20
+ const version = getPackageVersion();
21
+ return `<!-- Auto-generated by ts-procedures@${version}. Updated on npm install/update. Do not edit. -->\n\n`;
22
+ }
23
+
24
+ function stripFrontmatter(content) {
25
+ const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
26
+ return match ? match[1].trim() : content.trim();
27
+ }
28
+
29
+ function ensureDir(dir) {
30
+ if (!existsSync(dir)) {
31
+ mkdirSync(dir, { recursive: true });
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Install Claude Code integration files into a project's .claude/ directory.
37
+ *
38
+ * Creates:
39
+ * .claude/rules/ts-procedures.md — Framework reference (always loaded in context)
40
+ * .claude/commands/ts-procedures-scaffold.md — Scaffold command (/project:ts-procedures-scaffold)
41
+ * .claude/commands/ts-procedures-review.md — Review command (/project:ts-procedures-review)
42
+ * .claude/agents/ts-procedures-architect.md — Architecture planning agent
43
+ *
44
+ * @param {string} projectRoot — Absolute path to the consuming project's root
45
+ * @returns {{ files: string[] }} — List of created/updated file paths (relative to projectRoot)
46
+ */
47
+ export function installClaude(projectRoot) {
48
+ const claudeDir = join(projectRoot, '.claude');
49
+ const rulesDir = join(claudeDir, 'rules');
50
+ const commandsDir = join(claudeDir, 'commands');
51
+ const agentsDir = join(claudeDir, 'agents');
52
+
53
+ ensureDir(rulesDir);
54
+ ensureDir(commandsDir);
55
+ ensureDir(agentsDir);
56
+
57
+ const autoHeader = makeAutoHeader();
58
+ const files = [];
59
+
60
+ // 1. Rules file — framework reference (always loaded in context)
61
+ const guideSkill = readFileSync(join(SKILLS_DIR, 'guide', 'SKILL.md'), 'utf-8');
62
+ const guideBody = stripFrontmatter(guideSkill)
63
+ // Remove the "Supporting Files" section — replaced by "Detailed Reference" below
64
+ .replace(/\n## Supporting Files[\s\S]*$/, '');
65
+
66
+ const rulesContent = autoHeader + guideBody + `
67
+
68
+ ## Detailed Reference
69
+
70
+ For complete API details, patterns, and anti-patterns, read the files in:
71
+ \`node_modules/ts-procedures/agent_config/claude-code/skills/guide/\`
72
+
73
+ - \`api-reference.md\` — Full API reference for Procedures, Create, CreateStream, errors, schema, HTTP implementations
74
+ - \`patterns.md\` — Prescribed patterns with code examples
75
+ - \`anti-patterns.md\` — Common mistakes to avoid with fixes
76
+ `;
77
+
78
+ writeFileSync(join(rulesDir, 'ts-procedures.md'), rulesContent, 'utf-8');
79
+ files.push('.claude/rules/ts-procedures.md');
80
+
81
+ // 2. Scaffold command
82
+ const scaffoldSkill = readFileSync(join(SKILLS_DIR, 'scaffold', 'SKILL.md'), 'utf-8');
83
+ const scaffoldBody = stripFrontmatter(scaffoldSkill)
84
+ .replace(
85
+ 'Read the template file from `templates/<type>.md` in this skill directory.',
86
+ 'Read the template file from `node_modules/ts-procedures/agent_config/claude-code/skills/scaffold/templates/<type>.md`.'
87
+ );
88
+
89
+ writeFileSync(join(commandsDir, 'ts-procedures-scaffold.md'), autoHeader + scaffoldBody, 'utf-8');
90
+ files.push('.claude/commands/ts-procedures-scaffold.md');
91
+
92
+ // 3. Review command
93
+ const reviewSkill = readFileSync(join(SKILLS_DIR, 'review', 'SKILL.md'), 'utf-8');
94
+ const reviewBody = stripFrontmatter(reviewSkill)
95
+ .replaceAll(
96
+ '`checklist.md`',
97
+ '`node_modules/ts-procedures/agent_config/claude-code/skills/review/checklist.md`'
98
+ );
99
+
100
+ writeFileSync(join(commandsDir, 'ts-procedures-review.md'), autoHeader + reviewBody, 'utf-8');
101
+ files.push('.claude/commands/ts-procedures-review.md');
102
+
103
+ // 4. Architect agent
104
+ const architectAgent = readFileSync(join(AGENTS_DIR, 'ts-procedures-architect.md'), 'utf-8');
105
+ writeFileSync(join(agentsDir, 'ts-procedures-architect.md'), autoHeader + architectAgent, 'utf-8');
106
+ files.push('.claude/agents/ts-procedures-architect.md');
107
+
108
+ return { files };
109
+ }