ts-procedures 5.7.2 → 5.9.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 (50) hide show
  1. package/README.md +7 -1051
  2. package/agent_config/claude-code/skills/guide/api-reference.md +21 -16
  3. package/agent_config/claude-code/skills/guide/patterns.md +3 -1
  4. package/agent_config/copilot/copilot-instructions.md +7 -5
  5. package/agent_config/cursor/cursorrules +7 -5
  6. package/build/codegen/bin/cli.d.ts +2 -0
  7. package/build/codegen/bin/cli.js +21 -10
  8. package/build/codegen/bin/cli.js.map +1 -1
  9. package/build/codegen/bin/cli.test.js +44 -2
  10. package/build/codegen/bin/cli.test.js.map +1 -1
  11. package/build/codegen/emit-errors.d.ts +4 -1
  12. package/build/codegen/emit-errors.js +11 -5
  13. package/build/codegen/emit-errors.js.map +1 -1
  14. package/build/codegen/emit-errors.test.js +37 -0
  15. package/build/codegen/emit-errors.test.js.map +1 -1
  16. package/build/codegen/emit-index.d.ts +3 -1
  17. package/build/codegen/emit-index.js +6 -13
  18. package/build/codegen/emit-index.js.map +1 -1
  19. package/build/codegen/emit-index.test.js +23 -0
  20. package/build/codegen/emit-index.test.js.map +1 -1
  21. package/build/codegen/emit-scope.js +17 -13
  22. package/build/codegen/emit-scope.js.map +1 -1
  23. package/build/codegen/emit-scope.test.js +166 -0
  24. package/build/codegen/emit-scope.test.js.map +1 -1
  25. package/build/codegen/index.d.ts +1 -0
  26. package/build/codegen/index.js +1 -0
  27. package/build/codegen/index.js.map +1 -1
  28. package/build/codegen/naming.d.ts +7 -0
  29. package/build/codegen/naming.js +21 -0
  30. package/build/codegen/naming.js.map +1 -0
  31. package/build/codegen/naming.test.d.ts +1 -0
  32. package/build/codegen/naming.test.js +40 -0
  33. package/build/codegen/naming.test.js.map +1 -0
  34. package/build/codegen/pipeline.d.ts +1 -0
  35. package/build/codegen/pipeline.js +7 -3
  36. package/build/codegen/pipeline.js.map +1 -1
  37. package/build/codegen/pipeline.test.js +60 -0
  38. package/build/codegen/pipeline.test.js.map +1 -1
  39. package/docs/ai-agent-setup.md +61 -0
  40. package/docs/client-and-codegen.md +193 -0
  41. package/docs/core.md +473 -0
  42. package/docs/http-integrations.md +183 -0
  43. package/docs/streaming.md +199 -0
  44. package/docs/superpowers/plans/2026-03-30-client-codegen.md +2833 -0
  45. package/docs/superpowers/specs/2026-03-30-client-codegen-design.md +632 -0
  46. package/package.json +6 -1
  47. package/src/implementations/http/README.md +324 -0
  48. package/src/implementations/http/express-rpc/README.md +281 -0
  49. package/src/implementations/http/hono-rpc/README.md +358 -0
  50. package/src/implementations/http/hono-stream/README.md +525 -0
@@ -0,0 +1,632 @@
1
+ # ts-procedures Client Code Generation
2
+
3
+ **Date:** 2026-03-30
4
+ **Status:** Draft
5
+
6
+ ## Problem
7
+
8
+ Downstream applications that consume ts-procedures APIs must manually match server contracts on the client side. There is no type-safe bridge between the server's schema definitions and the client's HTTP calls. The DocRegistry already produces a JSON spec of all procedures (served via `GET /docs`), but this spec provides documentation only — no generated TypeScript types, no callable functions, no type safety.
9
+
10
+ ## Solution
11
+
12
+ A static code generation system that reads the DocRegistry's `DocEnvelope` output and produces per-scope TypeScript files containing typed params/response/yield types and callable functions for every procedure. The generated code imports a lightweight runtime client that handles HTTP execution, hook pipelines, and stream consumption. App developers plug in their own HTTP library (fetch, axios, etc.) via an adapter interface.
13
+
14
+ ## Architecture
15
+
16
+ Three layers, two new package exports:
17
+
18
+ ```
19
+ ┌─────────────────────────────────────────────────────────┐
20
+ │ ts-procedures/client (runtime — imported by app code) │
21
+ │ - ClientAdapter interface (request + stream methods) │
22
+ │ - Hook types (onBeforeRequest, onAfterResponse, etc.) │
23
+ │ - createClient() factory │
24
+ │ - Stream consumption utilities (SSE parser) │
25
+ │ - Client-side error classes │
26
+ └─────────────────────────────────────────────────────────┘
27
+
28
+ │ imports
29
+ ┌─────────────────────────────────────────────────────────┐
30
+ │ Generated code (per-scope .ts files in app repo) │
31
+ │ - TypeScript types (params, response, yield) via ajsc │
32
+ │ - Callable functions per procedure │
33
+ │ - One file per scope + barrel index.ts │
34
+ └─────────────────────────────────────────────────────────┘
35
+
36
+ │ produces
37
+ ┌─────────────────────────────────────────────────────────┐
38
+ │ ts-procedures/codegen (build-time tool) │
39
+ │ - generateClient() programmatic API │
40
+ │ - CLI: npx ts-procedures-codegen │
41
+ │ - Uses ajsc for JSON Schema → TypeScript conversion │
42
+ │ - Reads DocEnvelope from URL, file, or object │
43
+ └─────────────────────────────────────────────────────────┘
44
+ ```
45
+
46
+ - `ts-procedures/client` — zero external dependencies, pure TypeScript runtime
47
+ - `ts-procedures/codegen` — depends on `ajsc` for type generation, build-time only
48
+
49
+ ## Runtime Client (`ts-procedures/client`)
50
+
51
+ ### ClientInstance
52
+
53
+ The core internal type that generated scope bindings receive. This is constructed by `createClient` and passed to each `bind*Scope` factory:
54
+
55
+ ```ts
56
+ interface ClientInstance {
57
+ basePath: string;
58
+ adapter: ClientAdapter;
59
+ hooks: ClientHooks;
60
+
61
+ /** Execute a request/response procedure call */
62
+ call<TResponse>(
63
+ descriptor: CallDescriptor,
64
+ options?: ProcedureCallOptions,
65
+ ): Promise<TResponse>;
66
+
67
+ /** Execute a streaming procedure call */
68
+ stream<TYield, TReturn>(
69
+ descriptor: StreamDescriptor,
70
+ options?: ProcedureCallOptions,
71
+ ): TypedStream<TYield, TReturn>;
72
+ }
73
+
74
+ interface CallDescriptor {
75
+ name: string;
76
+ scope: string;
77
+ path: string;
78
+ method: string;
79
+ params: unknown;
80
+ }
81
+
82
+ interface StreamDescriptor extends CallDescriptor {
83
+ streamMode: 'sse' | 'text';
84
+ }
85
+
86
+ type ProcedureCallOptions = ClientHooks;
87
+ ```
88
+
89
+ ### Adapter Interface
90
+
91
+ App developers implement this to wrap their HTTP library of choice:
92
+
93
+ ```ts
94
+ interface ClientAdapter {
95
+ request(config: AdapterRequest): Promise<AdapterResponse>;
96
+ stream(config: AdapterRequest): Promise<AdapterStreamResponse>;
97
+ }
98
+
99
+ interface AdapterRequest {
100
+ url: string;
101
+ method: string;
102
+ headers?: Record<string, string>;
103
+ body?: unknown;
104
+ signal?: AbortSignal;
105
+ }
106
+
107
+ interface AdapterResponse {
108
+ status: number;
109
+ headers: Record<string, string>;
110
+ body: unknown;
111
+ }
112
+
113
+ interface AdapterStreamResponse {
114
+ status: number;
115
+ headers: Record<string, string>;
116
+ body: AsyncIterable<unknown>;
117
+ }
118
+ ```
119
+
120
+ Two methods — `request()` for RPC/API calls, `stream()` for SSE/text streaming. A user who does not use streaming only needs to implement `request()`.
121
+
122
+ The adapter's `stream()` method returns a raw `AsyncIterable<unknown>` of whatever the transport provides (raw SSE frames, text chunks, etc.). The runtime client handles parsing and type-narrowing — see the Stream Consumption section below.
123
+
124
+ ### Hook System
125
+
126
+ Global hooks configured on the client, with per-procedure overrides available on any call:
127
+
128
+ ```ts
129
+ interface ClientHooks {
130
+ onBeforeRequest?(context: BeforeRequestContext): BeforeRequestContext | Promise<BeforeRequestContext>;
131
+ onAfterResponse?(context: AfterResponseContext): void | Promise<void>;
132
+ onError?(context: ErrorContext): void | Promise<void>;
133
+ }
134
+
135
+ interface BeforeRequestContext {
136
+ procedureName: string;
137
+ scope: string;
138
+ request: AdapterRequest; // mutable — hooks modify this
139
+ }
140
+
141
+ interface AfterResponseContext {
142
+ procedureName: string;
143
+ scope: string;
144
+ request: AdapterRequest; // as-sent (after onBeforeRequest)
145
+ response: AdapterResponse; // mutable — hooks can transform
146
+ }
147
+
148
+ interface ErrorContext {
149
+ procedureName: string;
150
+ scope: string;
151
+ request: AdapterRequest;
152
+ error: unknown;
153
+ }
154
+ ```
155
+
156
+ **Execution order for request/response calls:**
157
+
158
+ 1. Build `AdapterRequest` from procedure metadata + caller args
159
+ 2. Run global `onBeforeRequest` then per-procedure `onBeforeRequest`
160
+ 3. Call `adapter.request()`
161
+ 4. If non-2xx and no hook swallows it, throw
162
+ 5. Run global `onAfterResponse` then per-procedure `onAfterResponse`
163
+ 6. Return typed result
164
+
165
+ **Execution order for streaming calls:**
166
+
167
+ 1. Build `AdapterRequest` from procedure metadata + caller args
168
+ 2. Run global `onBeforeRequest` then per-procedure `onBeforeRequest`
169
+ 3. Call `adapter.stream()`
170
+ 4. `onAfterResponse` fires immediately after the initial HTTP response (status + headers), before stream iteration begins — this lets hooks check status codes, read headers, or reject the stream early
171
+ 5. If non-2xx and no hook swallows it, throw
172
+ 6. Return `TypedStream` — iteration begins when the consumer starts reading
173
+ 7. If an error occurs mid-stream, `onError` fires with the error context
174
+
175
+ **Error strategy:** Hooks decide. Default behavior is throw on non-2xx. Generated callables return `Promise<T>` on the happy path. Apps implement their own error handling via `onAfterResponse` and `onError` hooks.
176
+
177
+ ### Stream Consumption and TypedStream
178
+
179
+ ```ts
180
+ interface TypedStream<TYield, TReturn = void> extends AsyncIterable<TYield> {
181
+ /** Resolves when the stream completes. Contains the final return value if the
182
+ * stream's returnType schema is defined, otherwise void.
183
+ * Rejects if the stream errors before completing. */
184
+ result: Promise<TReturn>;
185
+ }
186
+ ```
187
+
188
+ **How the runtime distinguishes yields from the final return:**
189
+
190
+ For SSE streams, the server's Hono stream builder wraps each yield in an SSE envelope (`{ data, event, id, retry }`). The runtime SSE parser strips this envelope and emits only the `data` payload as each `TYield` value. The final return value (if the procedure defines `returnType`) is sent as an SSE event with `event: 'return'` — the runtime captures this, resolves `.result`, and does not emit it as a yield.
191
+
192
+ For text streams, the runtime emits each chunk as a `TYield` value. `TReturn` is `void` for text streams (no final return mechanism).
193
+
194
+ **When `TReturn` is void:** If the server procedure has no `returnType` schema, the generated callable uses `TypedStream<TYield, void>` and `.result` resolves to `void` on stream completion. The `.result` promise always exists for uniform error handling (`.result.catch(...)`) regardless of whether there is a return value.
195
+
196
+ ### Path Parameter Interpolation
197
+
198
+ The runtime `call()` method interpolates path parameters using simple `:param` replacement:
199
+
200
+ ```ts
201
+ // path: '/users/:id/posts/:postId'
202
+ // params.pathParams: { id: '123', postId: '456' }
203
+ // result: '/users/123/posts/456'
204
+ ```
205
+
206
+ Each `:paramName` segment in the path is replaced with the corresponding value from `params.pathParams`. Values are URI-encoded. Unknown params are ignored. Missing params throw a client-side error.
207
+
208
+ ### createClient Factory
209
+
210
+ ```ts
211
+ import { createClient } from 'ts-procedures/client';
212
+ import { createScopeBindings } from './generated';
213
+
214
+ const client = createClient({
215
+ adapter: myFetchAdapter,
216
+ basePath: 'http://localhost:3000/api/v1',
217
+ scopes: createScopeBindings,
218
+ hooks: {
219
+ onBeforeRequest({ request, ...rest }) {
220
+ request.headers = {
221
+ ...request.headers,
222
+ Authorization: `Bearer ${getToken()}`,
223
+ };
224
+ return { ...rest, request };
225
+ },
226
+ onAfterResponse({ response }) {
227
+ if (response.status === 401) redirect('/login');
228
+ },
229
+ },
230
+ });
231
+
232
+ // Fully typed usage:
233
+ const user = await client.users.GetUser({ pathParams: { id: '123' } });
234
+
235
+ // Per-procedure hook override:
236
+ await client.users.GetUser({ pathParams: { id: '123' } }, {
237
+ onAfterResponse({ response }) {
238
+ const rateLimit = response.headers['x-rate-limit-remaining'];
239
+ },
240
+ });
241
+
242
+ // Streaming:
243
+ const stream = client.notifications.WatchNotifications({ accountId: 'abc' });
244
+ for await (const event of stream) {
245
+ console.log(event);
246
+ }
247
+ const finalResult = await stream.result;
248
+ ```
249
+
250
+ ## Generated Code
251
+
252
+ ### Scope Normalization
253
+
254
+ RPC and Stream routes define `scope` as `string | string[]` (e.g., `'users'` or `['admin', 'users']`). API routes will add an optional `scope` field. The codegen normalizes scope to a canonical string key for file grouping:
255
+
256
+ - **String scope:** used as-is → `'users'` → `users.ts`
257
+ - **Array scope:** joined with `-` → `['admin', 'users']` → `admin-users.ts`
258
+ - **Missing scope (API routes):** falls back to `'default'` → `default.ts`. The codegen emits a warning so developers know to add explicit scopes.
259
+
260
+ The `bind*Scope` function name and the property key in `createScopeBindings` use the normalized scope converted to camelCase (e.g., `admin-users` → `bindAdminUsersScope`, `adminUsers` property).
261
+
262
+ ### File Layout
263
+
264
+ One file per normalized scope, plus a barrel index:
265
+
266
+ ```
267
+ generated/
268
+ users.ts
269
+ billing.ts
270
+ admin-users.ts
271
+ notifications.ts
272
+ index.ts
273
+ ```
274
+
275
+ ### Scope File Structure
276
+
277
+ Each scope file contains types (via ajsc) and a `bind*Scope` factory:
278
+
279
+ ```ts
280
+ // Auto-generated by ts-procedures-codegen — do not edit
281
+ import type { ClientInstance, ProcedureCallOptions } from 'ts-procedures/client';
282
+
283
+ // ── Types ────────────────────────────────────────────────────
284
+
285
+ export type GetUserParams = {
286
+ pathParams: {
287
+ id: string;
288
+ };
289
+ query?: {
290
+ include?: string;
291
+ };
292
+ };
293
+
294
+ export type GetUserResponse = {
295
+ id: string;
296
+ name: string;
297
+ email: string;
298
+ createdAt: string;
299
+ };
300
+
301
+ export type CreateUserParams = {
302
+ body: {
303
+ name: string;
304
+ email: string;
305
+ role?: 'admin' | 'member';
306
+ };
307
+ };
308
+
309
+ export type CreateUserResponse = {
310
+ id: string;
311
+ name: string;
312
+ email: string;
313
+ role: 'admin' | 'member';
314
+ };
315
+
316
+ // ── Callables ────────────────────────────────────────────────
317
+
318
+ export function bindUsersScope(client: ClientInstance) {
319
+ return {
320
+ /** GET /api/v1/users/:id */
321
+ GetUser(params: GetUserParams, options?: ProcedureCallOptions): Promise<GetUserResponse> {
322
+ return client.call({
323
+ name: 'GetUser',
324
+ scope: 'users',
325
+ path: '/users/:id',
326
+ method: 'get',
327
+ params,
328
+ }, options);
329
+ },
330
+
331
+ /** POST /api/v1/users */
332
+ CreateUser(params: CreateUserParams, options?: ProcedureCallOptions): Promise<CreateUserResponse> {
333
+ return client.call({
334
+ name: 'CreateUser',
335
+ scope: 'users',
336
+ path: '/users',
337
+ method: 'post',
338
+ params,
339
+ }, options);
340
+ },
341
+ };
342
+ }
343
+ ```
344
+
345
+ ### Stream Scope Example
346
+
347
+ Generated yield types represent the **data payload only** — the SSE envelope (event, id, retry) is handled by the runtime and is not part of the generated type:
348
+
349
+ ```ts
350
+ // Auto-generated by ts-procedures-codegen — do not edit
351
+ import type { ClientInstance, ProcedureCallOptions, TypedStream } from 'ts-procedures/client';
352
+
353
+ export type WatchNotificationsParams = {
354
+ accountId: string;
355
+ };
356
+
357
+ export type WatchNotificationsYield = {
358
+ id: string;
359
+ type: 'invoice' | 'payment' | 'alert';
360
+ message: string;
361
+ };
362
+
363
+ export type WatchNotificationsReturn = {
364
+ total: number;
365
+ };
366
+
367
+ export function bindNotificationsScope(client: ClientInstance) {
368
+ return {
369
+ /** SSE /api/v1/notifications/WatchNotifications/1 */
370
+ WatchNotifications(
371
+ params: WatchNotificationsParams,
372
+ options?: ProcedureCallOptions,
373
+ ): TypedStream<WatchNotificationsYield, WatchNotificationsReturn> {
374
+ return client.stream({
375
+ name: 'WatchNotifications',
376
+ scope: 'notifications',
377
+ path: '/notifications/WatchNotifications/1',
378
+ method: 'post',
379
+ streamMode: 'sse',
380
+ params,
381
+ }, options);
382
+ },
383
+ };
384
+ }
385
+ ```
386
+
387
+ For streams with no `returnType` schema, the callable returns `TypedStream<TYield, void>`.
388
+
389
+ ### Barrel Index
390
+
391
+ ```ts
392
+ // Auto-generated by ts-procedures-codegen — do not edit
393
+ import type { ClientInstance } from 'ts-procedures/client';
394
+ import { bindUsersScope } from './users';
395
+ import { bindBillingScope } from './billing';
396
+ import { bindNotificationsScope } from './notifications';
397
+
398
+ export * from './users';
399
+ export * from './billing';
400
+ export * from './notifications';
401
+
402
+ export function createScopeBindings(client: ClientInstance) {
403
+ return {
404
+ users: bindUsersScope(client),
405
+ billing: bindBillingScope(client),
406
+ notifications: bindNotificationsScope(client),
407
+ };
408
+ }
409
+ ```
410
+
411
+ ## Code-Gen Engine (`ts-procedures/codegen`)
412
+
413
+ ### Programmatic API
414
+
415
+ ```ts
416
+ import { generateClient } from 'ts-procedures/codegen';
417
+
418
+ await generateClient({
419
+ // Input — one of:
420
+ url: 'http://localhost:3000/docs',
421
+ file: './docs.json',
422
+ envelope: docEnvelopeObject,
423
+
424
+ // Output
425
+ outDir: './src/generated/api',
426
+
427
+ // Options
428
+ ajsc: {
429
+ enumStyle: 'union',
430
+ depluralize: true,
431
+ },
432
+ });
433
+ ```
434
+
435
+ ### Pipeline
436
+
437
+ 1. **Resolve DocEnvelope** — fetch from URL, read file, or accept object directly
438
+ 2. **Normalize scopes** — for each route, normalize `scope` to a canonical string key (join arrays with `-`, default to `'default'` for API routes without scope)
439
+ 3. **Group routes by normalized scope**
440
+ 4. **For each scope, for each route:**
441
+ - Detect route kind via the `kind` discriminant field
442
+ - Convert each JSON Schema channel through ajsc `TypescriptConverter` to type strings
443
+ - For SSE streams: use the inner `data` schema from the yieldType SSE envelope, not the envelope itself
444
+ - Build callable function with hardcoded route metadata and typed signature
445
+ 5. **Assemble scope file** — imports + types + bind function
446
+ 6. **Generate index.ts** — barrel exports + `createScopeBindings`
447
+ 7. **Write all files** to `outDir`
448
+
449
+ ### Route Type Handling
450
+
451
+ | Route Kind | Detected By | Params Source | Return Source | Callable |
452
+ |---|---|---|---|---|
453
+ | `rpc` | `kind: 'rpc'` | `jsonSchema.body` → single params type | `jsonSchema.response` | `client.call()` |
454
+ | `api` | `kind: 'api'` | `jsonSchema.pathParams/query/body/headers` → structured params type | `jsonSchema.response` | `client.call()` |
455
+ | `stream` | `kind: 'stream'` | `jsonSchema.params` → single params type | `jsonSchema.yieldType` + `jsonSchema.returnType` | `client.stream()` |
456
+
457
+ For API routes with structured input, `client.call()` unpacks the params object:
458
+ - `pathParams` → interpolated into URL path via `:param` replacement, values URI-encoded
459
+ - `query` → serialized to query string
460
+ - `body` → request body (JSON-serialized)
461
+ - `headers` → merged into request headers
462
+
463
+ ### CLI
464
+
465
+ ```bash
466
+ # From URL (most common)
467
+ npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api
468
+
469
+ # From file
470
+ npx ts-procedures-codegen --file ./docs.json --out ./src/generated/api
471
+
472
+ # With ajsc options
473
+ npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api --enum-style union
474
+
475
+ # Watch mode — polls /docs endpoint, skips write if output unchanged
476
+ npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api --watch --interval 3000
477
+ ```
478
+
479
+ The `--watch` flag polls at a configurable interval (default 3000ms). It hashes the DocEnvelope response and skips file writes when the hash matches the previous run.
480
+
481
+ ### Code-Gen Error Handling
482
+
483
+ Fails with clear messages on:
484
+ - Routes missing `scope` where `kind` is `'rpc'` or `'stream'`: "Route 'X' has no scope — this should not happen for RPC/Stream routes"
485
+ - API routes missing `scope`: warning (falls back to `'default'`), not an error
486
+ - Unreachable URL or malformed JSON
487
+ - ajsc conversion failures: includes procedure name and the schema that failed
488
+ - Empty DocEnvelope (no routes)
489
+
490
+ ## Changes to Existing ts-procedures
491
+
492
+ ### Add Optional `scope` to API Route Config
493
+
494
+ API routes currently lack `scope`. Add it as **optional** to avoid a breaking change to all existing API builder consumers:
495
+
496
+ ```ts
497
+ // In src/implementations/types.ts
498
+ interface APIConfig {
499
+ path: string;
500
+ method: HttpMethod;
501
+ scope?: string; // NEW — optional, used by codegen for file grouping
502
+ successStatus?: number;
503
+ }
504
+ ```
505
+
506
+ The `scope` flows through to `APIHttpRouteDoc`. Existing API builder consumers are unaffected. The codegen warns (not errors) when API routes lack scope, falling back to the `'default'` group.
507
+
508
+ ### Add `kind` Discriminant to Route Docs
509
+
510
+ Add a `kind` field to each existing route doc interface. This is an additive change — existing fields are untouched:
511
+
512
+ ```ts
513
+ // In src/implementations/types.ts — additive fields only
514
+
515
+ interface RPCHttpRouteDoc extends RPCConfig {
516
+ kind: 'rpc'; // NEW
517
+ name: string;
518
+ path: string;
519
+ method: 'post';
520
+ jsonSchema: { /* unchanged */ };
521
+ }
522
+
523
+ interface APIHttpRouteDoc extends APIConfig {
524
+ kind: 'api'; // NEW
525
+ name: string;
526
+ fullPath: string;
527
+ jsonSchema: { /* unchanged */ };
528
+ }
529
+
530
+ interface StreamHttpRouteDoc extends RPCConfig {
531
+ kind: 'stream'; // NEW
532
+ name: string;
533
+ path: string;
534
+ methods: ('get' | 'post')[];
535
+ streamMode: StreamMode;
536
+ jsonSchema: { /* unchanged */ };
537
+ }
538
+ ```
539
+
540
+ Each builder sets `kind` when constructing its doc object. The `AnyHttpRouteDoc` union (`RPCHttpRouteDoc | APIHttpRouteDoc | StreamHttpRouteDoc`) can now be narrowed via `kind`.
541
+
542
+ **Note:** `StreamHttpRouteDoc` is currently defined in two places — `src/implementations/types.ts` (line 78) and `src/implementations/http/hono-stream/types.ts` (line 7). The `kind` field must be added to both. The implementer should consider consolidating to a single source (the canonical type in `types.ts`, re-exported by the hono-stream module).
543
+
544
+ ### Scope Type Consideration
545
+
546
+ `RPCConfig.scope` is typed `string | string[]`. The array form represents multi-segment scope paths (e.g., `['admin', 'users']` produces path `/admin/users/ProcName/1`). The codegen normalizes this to a string key for file grouping (joined with `-`). No changes to `RPCConfig` are needed — the normalization happens entirely in the codegen pipeline.
547
+
548
+ ### New Source Structure
549
+
550
+ ```
551
+ src/
552
+ client/
553
+ index.ts # createClient, ClientInstance
554
+ types.ts # ClientAdapter, hooks, AdapterRequest/Response, TypedStream
555
+ call.ts # request execution + hook pipeline
556
+ stream.ts # stream execution + SSE parsing
557
+ errors.ts # client-side error classes
558
+ codegen/
559
+ index.ts # generateClient() public API
560
+ pipeline.ts # orchestrates generation steps
561
+ resolve-envelope.ts # fetch URL / read file / accept object
562
+ group-routes.ts # normalize scopes + group routes
563
+ emit-scope.ts # generate a single scope file
564
+ emit-index.ts # generate the barrel index
565
+ emit-types.ts # ajsc integration
566
+ bin/
567
+ cli.ts # CLI entry point
568
+ ```
569
+
570
+ ### New Package Exports
571
+
572
+ Following the existing dual `types`/`import` pattern:
573
+
574
+ ```json
575
+ {
576
+ "exports": {
577
+ "./client": {
578
+ "types": "./build/client/index.d.ts",
579
+ "import": "./build/client/index.js"
580
+ },
581
+ "./codegen": {
582
+ "types": "./build/codegen/index.d.ts",
583
+ "import": "./build/codegen/index.js"
584
+ }
585
+ }
586
+ }
587
+ ```
588
+
589
+ A new `bin` entry for the CLI:
590
+
591
+ ```json
592
+ {
593
+ "bin": {
594
+ "ts-procedures-setup": "./agent_config/bin/setup.mjs",
595
+ "ts-procedures-codegen": "./build/codegen/bin/cli.js"
596
+ }
597
+ }
598
+ ```
599
+
600
+ ### New Dependency
601
+
602
+ - **ajsc** — added as an `optionalDependency` of ts-procedures (following the existing pattern used for `express` and `hono`). The codegen export uses a dynamic `import('ajsc')` so the dependency is only required when the codegen is actually invoked. Consumers who never run codegen do not need ajsc installed.
603
+
604
+ ### Impact on Existing Code
605
+
606
+ **Additive changes only — no breaking changes:**
607
+
608
+ - `APIConfig` gains an optional `scope` field (non-breaking)
609
+ - `RPCHttpRouteDoc`, `APIHttpRouteDoc`, `StreamHttpRouteDoc` each gain a `kind` field (additive)
610
+ - Each builder (hono-rpc, hono-api, hono-stream, express-rpc) must be updated to set `kind` when constructing route docs
611
+ - The `./http` types-only export (`build/implementations/http/types.d.ts`) will include the new `kind` fields on the union members — consumers who type-narrow `AnyHttpRouteDoc` may benefit from the new discriminant but are not required to use it
612
+ - Existing tests continue to pass; new tests are added for the `kind` field and `scope` on API routes
613
+
614
+ ## Decision Summary
615
+
616
+ | Decision | Choice |
617
+ |---|---|
618
+ | Trigger | CLI + programmatic API |
619
+ | File grouping | Intentional scope on all routes, array scopes joined with `-` |
620
+ | Route coverage | RPC + API + Stream from v1 |
621
+ | Adapter | Two-method: `request()` + `stream()` |
622
+ | Hooks | Global + per-procedure overrides |
623
+ | Hook timing (streams) | `onAfterResponse` fires on initial HTTP response; `onError` fires on mid-stream errors |
624
+ | Error strategy | Hooks decide, default throw, callables return `Promise<T>` |
625
+ | Runtime location | `ts-procedures/client` export |
626
+ | Codegen approach | Full static — types + callables in generated .ts files |
627
+ | Type generation | ajsc `TypescriptConverter` (optionalDependency, dynamic import) |
628
+ | Route discrimination | Explicit `kind` field on all route docs (additive) |
629
+ | Yield types | Data payload only — SSE envelope stripped by runtime |
630
+ | Stream completion | `TypedStream.result` promise; `event: 'return'` for SSE final value |
631
+ | API scope | Optional on `APIConfig`, defaults to `'default'` in codegen with warning |
632
+ | Breaking changes | None — all changes are additive to existing interfaces |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-procedures",
3
- "version": "5.7.2",
3
+ "version": "5.9.0",
4
4
  "description": "A TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition. Define your procedures once and get full type inference, runtime validation, and framework integration hooks.",
5
5
  "main": "build/exports.js",
6
6
  "types": "build/exports.d.ts",
@@ -59,7 +59,12 @@
59
59
  "files": [
60
60
  "assets",
61
61
  "build",
62
+ "docs",
62
63
  "agent_config",
64
+ "src/implementations/http/README.md",
65
+ "src/implementations/http/express-rpc/README.md",
66
+ "src/implementations/http/hono-rpc/README.md",
67
+ "src/implementations/http/hono-stream/README.md",
63
68
  "src/client/types.ts",
64
69
  "src/client/errors.ts",
65
70
  "src/client/request-builder.ts",