@vibesdotdev/client 0.1.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 (117) hide show
  1. package/SPEC.md +107 -0
  2. package/dist/factory.d.ts +34 -0
  3. package/dist/factory.d.ts.map +1 -0
  4. package/dist/factory.js +80 -0
  5. package/dist/factory.js.map +1 -0
  6. package/dist/index.d.ts +10 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +6 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/lib/client/core.d.ts +8 -0
  11. package/dist/lib/client/core.d.ts.map +1 -0
  12. package/dist/lib/client/core.js +5 -0
  13. package/dist/lib/client/core.js.map +1 -0
  14. package/dist/lib/client/internal/base-client.d.ts +19 -0
  15. package/dist/lib/client/internal/base-client.d.ts.map +1 -0
  16. package/dist/lib/client/internal/base-client.js +92 -0
  17. package/dist/lib/client/internal/base-client.js.map +1 -0
  18. package/dist/lib/client/internal/base-helpers.d.ts +9 -0
  19. package/dist/lib/client/internal/base-helpers.d.ts.map +1 -0
  20. package/dist/lib/client/internal/base-helpers.js +79 -0
  21. package/dist/lib/client/internal/base-helpers.js.map +1 -0
  22. package/dist/lib/client/internal/base-types.d.ts +35 -0
  23. package/dist/lib/client/internal/base-types.d.ts.map +1 -0
  24. package/dist/lib/client/internal/base-types.js +15 -0
  25. package/dist/lib/client/internal/base-types.js.map +1 -0
  26. package/dist/lib/client/internal/endpoint.d.ts +22 -0
  27. package/dist/lib/client/internal/endpoint.d.ts.map +1 -0
  28. package/dist/lib/client/internal/endpoint.js +35 -0
  29. package/dist/lib/client/internal/endpoint.js.map +1 -0
  30. package/dist/lib/client/internal/generator.d.ts +20 -0
  31. package/dist/lib/client/internal/generator.d.ts.map +1 -0
  32. package/dist/lib/client/internal/generator.js +173 -0
  33. package/dist/lib/client/internal/generator.js.map +1 -0
  34. package/dist/lib/client/internal/index.d.ts +5 -0
  35. package/dist/lib/client/internal/index.d.ts.map +1 -0
  36. package/dist/lib/client/internal/index.js +4 -0
  37. package/dist/lib/client/internal/index.js.map +1 -0
  38. package/dist/lib/client/internal/node/http2-fetch.node.d.ts +2 -0
  39. package/dist/lib/client/internal/node/http2-fetch.node.d.ts.map +1 -0
  40. package/dist/lib/client/internal/node/http2-fetch.node.js +131 -0
  41. package/dist/lib/client/internal/node/http2-fetch.node.js.map +1 -0
  42. package/dist/lib/client/internal/request-builder.d.ts +15 -0
  43. package/dist/lib/client/internal/request-builder.d.ts.map +1 -0
  44. package/dist/lib/client/internal/request-builder.js +158 -0
  45. package/dist/lib/client/internal/request-builder.js.map +1 -0
  46. package/dist/lib/client/internal/sse-stream.d.ts +23 -0
  47. package/dist/lib/client/internal/sse-stream.d.ts.map +1 -0
  48. package/dist/lib/client/internal/sse-stream.js +110 -0
  49. package/dist/lib/client/internal/sse-stream.js.map +1 -0
  50. package/dist/lib/client/internal/vibes-client.d.ts +32 -0
  51. package/dist/lib/client/internal/vibes-client.d.ts.map +1 -0
  52. package/dist/lib/client/internal/vibes-client.js +120 -0
  53. package/dist/lib/client/internal/vibes-client.js.map +1 -0
  54. package/dist/lib/client/internal/wrap-fetch.d.ts +6 -0
  55. package/dist/lib/client/internal/wrap-fetch.d.ts.map +1 -0
  56. package/dist/lib/client/internal/wrap-fetch.js +46 -0
  57. package/dist/lib/client/internal/wrap-fetch.js.map +1 -0
  58. package/dist/lib/client/node.d.ts +5 -0
  59. package/dist/lib/client/node.d.ts.map +1 -0
  60. package/dist/lib/client/node.js +33 -0
  61. package/dist/lib/client/node.js.map +1 -0
  62. package/dist/lib/client/types.d.ts +145 -0
  63. package/dist/lib/client/types.d.ts.map +1 -0
  64. package/dist/lib/client/types.js +21 -0
  65. package/dist/lib/client/types.js.map +1 -0
  66. package/dist/plugin.d.ts +19 -0
  67. package/dist/plugin.d.ts.map +1 -0
  68. package/dist/plugin.js +80 -0
  69. package/dist/plugin.js.map +1 -0
  70. package/dist/schemas.d.ts +90 -0
  71. package/dist/schemas.d.ts.map +1 -0
  72. package/dist/schemas.js +9 -0
  73. package/dist/schemas.js.map +1 -0
  74. package/dist/sse-client.d.ts +39 -0
  75. package/dist/sse-client.d.ts.map +1 -0
  76. package/dist/sse-client.js +124 -0
  77. package/dist/sse-client.js.map +1 -0
  78. package/dist/tools/api-request/api-request.descriptor.d.ts +48 -0
  79. package/dist/tools/api-request/api-request.descriptor.d.ts.map +1 -0
  80. package/dist/tools/api-request/api-request.descriptor.js +27 -0
  81. package/dist/tools/api-request/api-request.descriptor.js.map +1 -0
  82. package/dist/tools/api-request/api-request.impl.consumer.d.ts +13 -0
  83. package/dist/tools/api-request/api-request.impl.consumer.d.ts.map +1 -0
  84. package/dist/tools/api-request/api-request.impl.consumer.js +51 -0
  85. package/dist/tools/api-request/api-request.impl.consumer.js.map +1 -0
  86. package/dist/tools/api-request/index.d.ts +5 -0
  87. package/dist/tools/api-request/index.d.ts.map +1 -0
  88. package/dist/tools/api-request/index.js +4 -0
  89. package/dist/tools/api-request/index.js.map +1 -0
  90. package/dist/tools/api-request/schemas/index.d.ts +33 -0
  91. package/dist/tools/api-request/schemas/index.d.ts.map +1 -0
  92. package/dist/tools/api-request/schemas/index.js +24 -0
  93. package/dist/tools/api-request/schemas/index.js.map +1 -0
  94. package/package.json +99 -0
  95. package/src/factory.ts +114 -0
  96. package/src/index.ts +15 -0
  97. package/src/lib/client/core.ts +13 -0
  98. package/src/lib/client/internal/base-client.ts +107 -0
  99. package/src/lib/client/internal/base-helpers.ts +74 -0
  100. package/src/lib/client/internal/base-types.ts +42 -0
  101. package/src/lib/client/internal/endpoint.ts +51 -0
  102. package/src/lib/client/internal/generator.ts +181 -0
  103. package/src/lib/client/internal/index.ts +4 -0
  104. package/src/lib/client/internal/node/http2-fetch.node.ts +138 -0
  105. package/src/lib/client/internal/request-builder.ts +147 -0
  106. package/src/lib/client/internal/sse-stream.ts +130 -0
  107. package/src/lib/client/internal/vibes-client.ts +167 -0
  108. package/src/lib/client/internal/wrap-fetch.ts +59 -0
  109. package/src/lib/client/node.ts +36 -0
  110. package/src/lib/client/types.ts +156 -0
  111. package/src/plugin.ts +104 -0
  112. package/src/schemas.ts +91 -0
  113. package/src/sse-client.ts +155 -0
  114. package/src/tools/api-request/api-request.descriptor.ts +28 -0
  115. package/src/tools/api-request/api-request.impl.consumer.ts +66 -0
  116. package/src/tools/api-request/index.ts +4 -0
  117. package/src/tools/api-request/schemas/index.ts +29 -0
@@ -0,0 +1,167 @@
1
+ import type { AuthConfig, ClientConfig, ClientHooks, RequestOptions } from '../types.ts';
2
+ import { APIGenerator, type OpenAPISpec } from './generator.ts';
3
+ import { getRuntimeEnv } from '@vibesdotdev/runtime';
4
+ import { RequestBuilder } from './request-builder.ts';
5
+ import { streamSSE, type SSEEvent, type SSEStreamOptions } from './sse-stream.ts';
6
+ import { createHookedFetch } from './wrap-fetch.ts';
7
+
8
+ interface ResolvedClientConfig {
9
+ baseUrl: string;
10
+ auth: AuthConfig;
11
+ fetch: typeof fetch;
12
+ hooks: ClientHooks;
13
+ timeout: number;
14
+ debug: boolean;
15
+ }
16
+
17
+ export class VibesClient {
18
+ private config: ResolvedClientConfig;
19
+ private requestBuilder: RequestBuilder;
20
+ private apiGenerator: APIGenerator;
21
+ private apis: Record<string, unknown> = {};
22
+ private specPromise: Promise<OpenAPISpec> | null = null;
23
+
24
+ constructor(config: ClientConfig = {}) {
25
+ const baseUrl =
26
+ config.baseUrl ||
27
+ getRuntimeEnv('VIBES_API_URL') ||
28
+ 'http://localhost:5173';
29
+ const fetchImpl = config.fetch || globalThis.fetch?.bind(globalThis);
30
+ if (!fetchImpl)
31
+ throw new Error('Fetch is not available. Please provide a fetch implementation.');
32
+ const hooks = config.hooks || {};
33
+ this.config = {
34
+ baseUrl,
35
+ auth: config.auth || { type: 'none' },
36
+ fetch: fetchImpl as typeof fetch,
37
+ hooks,
38
+ timeout: config.timeout || 30000,
39
+ debug: config.debug || false
40
+ };
41
+ const hookedFetch = createHookedFetch({
42
+ fetchImpl: this.config.fetch,
43
+ hooks: this.config.hooks
44
+ });
45
+ this.requestBuilder = new RequestBuilder(
46
+ this.config.baseUrl,
47
+ this.config.auth,
48
+ hookedFetch as typeof fetch,
49
+ this.config.timeout,
50
+ this.config.debug
51
+ );
52
+ this.apiGenerator = new APIGenerator(this.requestBuilder);
53
+ }
54
+
55
+ private async initialize(): Promise<void> {
56
+ try {
57
+ const spec = await this.getOpenAPISpec();
58
+ this.buildAPIs(spec);
59
+ } catch (error) {
60
+ const shouldWarn =
61
+ this.config.debug ||
62
+ (typeof process !== 'undefined' && process.env?.VIBES_CLIENT_WARN_OAS === 'true');
63
+ if (shouldWarn) {
64
+ console.warn('Failed to load OpenAPI spec, client will have limited functionality:', error);
65
+ }
66
+ }
67
+ }
68
+
69
+ getOpenAPISpec(): Promise<OpenAPISpec> {
70
+ if (!this.specPromise) this.specPromise = this.fetchOpenAPISpec();
71
+ return this.specPromise;
72
+ }
73
+
74
+ private async fetchOpenAPISpec(): Promise<OpenAPISpec> {
75
+ const response = await this.config.fetch(`${this.config.baseUrl}/api/docs/openapi.json`);
76
+ if (!response.ok) throw new Error(`Failed to fetch OpenAPI spec: ${response.statusText}`);
77
+ return response.json() as Promise<OpenAPISpec>;
78
+ }
79
+
80
+ private buildAPIs(spec: OpenAPISpec): void {
81
+ const apis = this.apiGenerator.generateAPIs(spec);
82
+ for (const [name, api] of Object.entries(apis)) {
83
+ this.apis[name] = api;
84
+ Object.defineProperty(this, name, {
85
+ get: () => this.apis[name],
86
+ enumerable: true,
87
+ configurable: true
88
+ });
89
+ }
90
+ }
91
+
92
+ request<T = unknown>(
93
+ path: string,
94
+ options: {
95
+ method?: string;
96
+ params?: Record<string, string | number | boolean>;
97
+ query?: Record<string, string | number | boolean | string[] | undefined>;
98
+ body?: unknown;
99
+ headers?: Record<string, string>;
100
+ timeout?: number;
101
+ } = {}
102
+ ): Promise<T> {
103
+ return this.requestBuilder.request({
104
+ path,
105
+ method: options.method || 'GET',
106
+ params: options.params,
107
+ query: options.query,
108
+ body: options.body,
109
+ headers: options.headers,
110
+ timeout: options.timeout
111
+ });
112
+ }
113
+
114
+ setAuth(auth: AuthConfig): void {
115
+ this.config.auth = auth;
116
+ const hookedFetch = createHookedFetch({
117
+ fetchImpl: this.config.fetch,
118
+ hooks: this.config.hooks
119
+ });
120
+ this.requestBuilder = new RequestBuilder(
121
+ this.config.baseUrl,
122
+ auth,
123
+ hookedFetch as typeof fetch,
124
+ this.config.timeout,
125
+ this.config.debug
126
+ );
127
+ this.apiGenerator = new APIGenerator(this.requestBuilder);
128
+ if (this.specPromise) this.specPromise.then((spec) => this.buildAPIs(spec));
129
+ }
130
+
131
+ getResources(): string[] {
132
+ return Object.keys(this.apis);
133
+ }
134
+
135
+ hasResource(name: string): boolean {
136
+ return name in this.apis;
137
+ }
138
+
139
+ get<T = unknown>(endpoint: string, options?: RequestOptions): Promise<T> {
140
+ return this.request<T>(endpoint, { ...options, method: 'GET' });
141
+ }
142
+
143
+ post<T = unknown>(endpoint: string, body?: unknown, options?: RequestOptions): Promise<T> {
144
+ return this.request<T>(endpoint, { ...options, method: 'POST', body });
145
+ }
146
+
147
+ put<T = unknown>(endpoint: string, body?: unknown, options?: RequestOptions): Promise<T> {
148
+ return this.request<T>(endpoint, { ...options, method: 'PUT', body });
149
+ }
150
+
151
+ delete<T = unknown>(endpoint: string, options?: RequestOptions): Promise<T> {
152
+ return this.request<T>(endpoint, { ...options, method: 'DELETE' });
153
+ }
154
+
155
+ async *stream<T = unknown>(
156
+ path: string,
157
+ options: SSEStreamOptions = {}
158
+ ): AsyncIterableIterator<SSEEvent<T>> {
159
+ yield* streamSSE<T>({
160
+ baseUrl: this.config.baseUrl,
161
+ auth: this.config.auth,
162
+ fetchImpl: this.config.fetch,
163
+ path,
164
+ options
165
+ });
166
+ }
167
+ }
@@ -0,0 +1,59 @@
1
+ import { ClientError, type ClientHooks } from '../types.ts';
2
+
3
+ export function createHookedFetch(args: {
4
+ fetchImpl: typeof fetch;
5
+ hooks: ClientHooks;
6
+ }): (input: Parameters<typeof fetch>[0], init?: RequestInit) => Promise<Response> {
7
+ return async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
8
+ if (!args.hooks.beforeRequest) {
9
+ let response: Response;
10
+ try {
11
+ response = await args.fetchImpl(input, init);
12
+ } catch (error) {
13
+ if (args.hooks.onError) {
14
+ await args.hooks.onError(
15
+ new ClientError(
16
+ error instanceof Error ? error.message : 'Network error',
17
+ 0,
18
+ undefined,
19
+ undefined
20
+ )
21
+ );
22
+ }
23
+ throw error;
24
+ }
25
+ if (args.hooks.afterResponse) response = await args.hooks.afterResponse(response);
26
+ return response;
27
+ }
28
+ const requestInput = input instanceof URL
29
+ ? new Request(input.href, init)
30
+ : typeof input === 'string'
31
+ ? new Request(input, init)
32
+ : new Request(input, init);
33
+ const modifiedRequest = await args.hooks.beforeRequest(requestInput);
34
+ const modifiedInit: RequestInit = {
35
+ ...init,
36
+ method: modifiedRequest.method,
37
+ headers: Object.fromEntries(modifiedRequest.headers.entries()),
38
+ ...(modifiedRequest.body !== requestInput.body && { body: modifiedRequest.body })
39
+ };
40
+ let response: Response;
41
+ try {
42
+ response = await args.fetchImpl(modifiedRequest.url, modifiedInit);
43
+ } catch (error) {
44
+ if (args.hooks.onError) {
45
+ await args.hooks.onError(
46
+ new ClientError(
47
+ error instanceof Error ? error.message : 'Network error',
48
+ 0,
49
+ undefined,
50
+ undefined
51
+ )
52
+ );
53
+ }
54
+ throw error;
55
+ }
56
+ if (args.hooks.afterResponse) response = await args.hooks.afterResponse(response);
57
+ return response;
58
+ };
59
+ }
@@ -0,0 +1,36 @@
1
+ import { getRuntimeEnv } from '@vibesdotdev/runtime';
2
+ import type { ClientConfig } from './types.ts';
3
+ import { VibesClient } from './internal/vibes-client.ts';
4
+
5
+ export { createHttp2Fetch } from './internal/node/http2-fetch.node.ts';
6
+
7
+ async function resolveNodeFetch(baseUrl: string): Promise<typeof globalThis.fetch> {
8
+ const isHttps = baseUrl.startsWith('https://');
9
+ const isLocal =
10
+ baseUrl.includes('.local') || baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1');
11
+ if (isHttps && isLocal) {
12
+ try {
13
+ if (!getRuntimeEnv('REJECT_UNAUTHORIZED')) {
14
+ globalThis.process?.['env'] && (globalThis.process.env.REJECT_UNAUTHORIZED = 'false');
15
+ }
16
+ const { createHttp2Fetch } = await import('./internal/node/http2-fetch.node.js');
17
+ return createHttp2Fetch(baseUrl) as unknown as typeof globalThis.fetch;
18
+ } catch {
19
+ // Fallback to regular fetch if h2 module unavailable
20
+ }
21
+ }
22
+ return globalThis.fetch;
23
+ }
24
+
25
+ export async function createCLIClient(config?: Partial<ClientConfig>): Promise<VibesClient> {
26
+ const baseUrl = config?.baseUrl || getRuntimeEnv('VIBES_API_URL') || 'http://localhost:5173';
27
+ const fetchImpl = config?.fetch || (await resolveNodeFetch(baseUrl));
28
+ const clientConfig: ClientConfig = {
29
+ baseUrl,
30
+ auth: config?.auth || { type: 'none' },
31
+ fetch: fetchImpl,
32
+ debug: getRuntimeEnv('VIBES_DEBUG') === 'true',
33
+ ...config
34
+ };
35
+ return new VibesClient(clientConfig);
36
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Vibes Client Types
3
+ *
4
+ * Configuration, errors, and type contracts for the VibesClient HTTP API client.
5
+ * Extracted from libraries/vibeskit (PR #447 migration) to canonicalize as a
6
+ * pure-isomorphic @vibesdotdev/client surface.
7
+ */
8
+
9
+ /** Authentication configuration */
10
+ export interface AuthConfig {
11
+ /** Authentication type */
12
+ type: 'apiKey' | 'session' | 'bearer' | 'none';
13
+ /** Static credentials */
14
+ credentials?: string;
15
+ /** Dynamic token provider */
16
+ tokenProvider?: () => Promise<string | null>;
17
+ /** Session refresh handler for expired sessions */
18
+ refreshHandler?: () => Promise<AuthConfig | null>;
19
+ }
20
+
21
+ /** Client lifecycle hooks */
22
+ export interface ClientHooks {
23
+ /** Called before each request */
24
+ beforeRequest?: (request: Request) => Promise<Request> | Request;
25
+ /** Called after each response */
26
+ afterResponse?: (response: Response) => Promise<Response> | Response;
27
+ /** Called on request error */
28
+ onError?: (error: ClientError) => Promise<void> | void;
29
+ }
30
+
31
+ /** Request options */
32
+ export interface RequestOptions {
33
+ /** HTTP method */
34
+ method: string;
35
+ /** Request path (will be appended to baseUrl) */
36
+ path: string;
37
+ /** URL parameters */
38
+ params?: Record<string, string | number | boolean>;
39
+ /** Query parameters */
40
+ query?: Record<string, string | number | boolean | string[] | undefined>;
41
+ /** Request body */
42
+ body?: unknown;
43
+ /** Additional headers */
44
+ headers?: Record<string, string>;
45
+ /** Override timeout for this request */
46
+ timeout?: number;
47
+ }
48
+
49
+ /** Client configuration options */
50
+ export interface ClientConfig {
51
+ /** Base URL for API requests */
52
+ baseUrl?: string;
53
+ /** Authentication configuration */
54
+ auth?: AuthConfig;
55
+ /** Custom fetch implementation */
56
+ fetch?: typeof fetch;
57
+ /** Request/response hooks */
58
+ hooks?: ClientHooks;
59
+ /** Request timeout in milliseconds */
60
+ timeout?: number;
61
+ /** Enable debug logging */
62
+ debug?: boolean;
63
+ }
64
+
65
+ /** Client error with additional context */
66
+ export class ClientError extends Error {
67
+ constructor(
68
+ message: string,
69
+ public status?: number,
70
+ public response?: Response,
71
+ public request?: RequestOptions
72
+ ) {
73
+ super(message);
74
+ this.name = 'ClientError';
75
+ }
76
+ }
77
+
78
+ /** Paginated response wrapper */
79
+ export interface PaginatedResponse<T> {
80
+ data: T[];
81
+ total: number;
82
+ page: number;
83
+ pageSize: number;
84
+ hasMore: boolean;
85
+ }
86
+
87
+ /** Standard list parameters */
88
+ export interface ListParams {
89
+ /** Page number (1-based) */
90
+ page?: number;
91
+ /** Items per page */
92
+ limit?: number;
93
+ /** Sort field */
94
+ sortBy?: string;
95
+ /** Sort direction */
96
+ sortOrder?: 'asc' | 'desc';
97
+ /** Search query */
98
+ search?: string;
99
+ /** Additional filters */
100
+ filters?: Record<string, unknown>;
101
+ }
102
+
103
+ /** OpenAPI path info (generic -- no openapi-types dependency) */
104
+ export interface PathInfo {
105
+ path: string;
106
+ method: string;
107
+ operationId?: string;
108
+ parameters?: Record<string, unknown>[];
109
+ requestBody?: Record<string, unknown>;
110
+ responses?: Record<string, unknown>;
111
+ tags?: string[];
112
+ summary?: string;
113
+ description?: string;
114
+ }
115
+
116
+ /** Grouped API resources */
117
+ export interface APIResource {
118
+ name: string;
119
+ basePath: string;
120
+ operations: Map<string, PathInfo>;
121
+ }
122
+
123
+ /**
124
+ * Migration debt: ClientUser is an auth-domain type owned by @vibesdotdev/auth.
125
+ * Defined here temporarily so @vibesdotdev/kit/client consumers can migrate.
126
+ * Target: move to packages/auth/src/shared/client-user.ts and re-export from client/core.
127
+ */
128
+ export interface ClientUser {
129
+ id: string;
130
+ email: string;
131
+ displayName?: string | null;
132
+ avatarUrl?: string | null;
133
+ isSuperuser?: boolean;
134
+ roles?: Array<{
135
+ id: string;
136
+ name: string;
137
+ description: string | null;
138
+ isSystemRole: boolean;
139
+ }>;
140
+ permissions?: string[];
141
+ }
142
+
143
+ /**
144
+ * Type helpers for schema conversion
145
+ */
146
+ export type SchemaToType<T> = T extends { type: 'string' }
147
+ ? string
148
+ : T extends { type: 'number' }
149
+ ? number
150
+ : T extends { type: 'boolean' }
151
+ ? boolean
152
+ : T extends { type: 'array'; items: infer I }
153
+ ? Array<SchemaToType<I>>
154
+ : T extends { type: 'object'; properties: infer P }
155
+ ? { [K in keyof P]: SchemaToType<P[K]> }
156
+ : unknown;
package/src/plugin.ts ADDED
@@ -0,0 +1,104 @@
1
+ import { createRuntimePlugin, loader } from '@vibesdotdev/runtime';
2
+ import type { RuntimeDescriptor } from '@vibesdotdev/runtime/schemas/descriptor';
3
+ import {
4
+ ApiClientDescriptorSchema,
5
+ type ApiClientDescriptor,
6
+ type ApiClientImplementation,
7
+ type ApiStreamEvent
8
+ } from './schemas.ts';
9
+ import { vibesApiClient } from './factory.ts';
10
+ import type { KindContext, RuntimeKindDescriptorRecord } from '@vibesdotdev/runtime/schemas/kind';
11
+ import apiRequestToolDescriptor from './tools/api-request/api-request.descriptor.ts';
12
+
13
+ function isApiClientDescriptor(descriptor: RuntimeDescriptor): descriptor is ApiClientDescriptor {
14
+ return descriptor.kind === 'api/client' && typeof descriptor.id === 'string';
15
+ }
16
+
17
+ /**
18
+ * Default API client implementation.
19
+ *
20
+ * When no specific client implementation is registered for a descriptor,
21
+ * this default constructs a client using the descriptor's baseUrl.
22
+ */
23
+ class DefaultApiClient implements ApiClientImplementation {
24
+ private client: ApiClientImplementation;
25
+
26
+ constructor(descriptor: RuntimeDescriptor, _context: KindContext) {
27
+ if (!isApiClientDescriptor(descriptor)) {
28
+ throw new Error(`Invalid api/client descriptor '${descriptor.id ?? 'unknown'}'`);
29
+ }
30
+
31
+ this.client = vibesApiClient({
32
+ id: descriptor.id,
33
+ baseUrl: descriptor.baseUrl,
34
+ timeout: descriptor.timeout,
35
+ debug: descriptor.debug
36
+ });
37
+ }
38
+
39
+ get<T = unknown>(endpoint: string, options?: Parameters<ApiClientImplementation['get']>[1]): Promise<T> {
40
+ return this.client.get<T>(endpoint, options);
41
+ }
42
+
43
+ post<T = unknown>(endpoint: string, body?: unknown, options?: Parameters<ApiClientImplementation['post']>[2]): Promise<T> {
44
+ return this.client.post<T>(endpoint, body, options);
45
+ }
46
+
47
+ put<T = unknown>(endpoint: string, body?: unknown, options?: Parameters<ApiClientImplementation['put']>[2]): Promise<T> {
48
+ return this.client.put<T>(endpoint, body, options);
49
+ }
50
+
51
+ patch<T = unknown>(endpoint: string, body?: unknown, options?: Parameters<ApiClientImplementation['patch']>[2]): Promise<T> {
52
+ return this.client.patch<T>(endpoint, body, options);
53
+ }
54
+
55
+ delete<T = unknown>(endpoint: string, options?: Parameters<ApiClientImplementation['delete']>[1]): Promise<T> {
56
+ return this.client.delete<T>(endpoint, options);
57
+ }
58
+
59
+ request<T = unknown>(endpoint: string, options?: Parameters<ApiClientImplementation['request']>[1]): Promise<T> {
60
+ return this.client.request<T>(endpoint, options);
61
+ }
62
+
63
+ stream<T = unknown>(
64
+ endpoint: string,
65
+ options?: Parameters<ApiClientImplementation['stream']>[1]
66
+ ): AsyncIterableIterator<ApiStreamEvent<T>> {
67
+ return this.client.stream<T>(endpoint, options) as AsyncIterableIterator<ApiStreamEvent<T>>;
68
+ }
69
+ }
70
+
71
+ const apiClientKind = {
72
+ id: 'api/client',
73
+ descriptorSchema: ApiClientDescriptorSchema,
74
+ defaultImplementation: DefaultApiClient
75
+ } satisfies RuntimeKindDescriptorRecord<ApiClientImplementation>;
76
+
77
+ /**
78
+ * Client plugin — registers the `api/client` kind with the runtime.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * import { getVibesRuntime } from '@vibesdotdev/runtime';
83
+ * import clientPlugin from '@vibesdotdev/client/plugin';
84
+ *
85
+ * const runtime = getVibesRuntime();
86
+ * await runtime.registerPlugin(clientPlugin);
87
+ *
88
+ * // Query a registered API client
89
+ * const authClient = await runtime.query('api/client').withId('auth').resolve();
90
+ * await authClient.get('/api/health');
91
+ * ```
92
+ */
93
+ export default createRuntimePlugin({
94
+ id: 'client',
95
+ name: 'API Client Module',
96
+ description: 'Runtime API client kind with environment-aware factories',
97
+ kinds: [apiClientKind],
98
+ descriptors: [apiRequestToolDescriptor],
99
+ loaders: [
100
+ loader('api-request', { kind: 'tools/tool', scope: 'consumer' }, () =>
101
+ import('./tools/api-request/api-request.impl.consumer.js')
102
+ )
103
+ ]
104
+ });
package/src/schemas.ts ADDED
@@ -0,0 +1,91 @@
1
+ import * as z from 'zod/v4';
2
+ import {
3
+ RuntimeDescriptorSchema,
4
+ type RuntimeDescriptor
5
+ } from '@vibesdotdev/runtime/schemas/descriptor';
6
+
7
+ /**
8
+ * API Client descriptor — metadata for a registered API client asset.
9
+ */
10
+ export interface ApiClientDescriptor extends RuntimeDescriptor {
11
+ kind: 'api/client';
12
+ /** Base URL for this client's API */
13
+ baseUrl?: string;
14
+ /** Default timeout in milliseconds */
15
+ timeout?: number;
16
+ /** Whether to enable debug logging */
17
+ debug?: boolean;
18
+ }
19
+
20
+ export const ApiClientDescriptorSchema = RuntimeDescriptorSchema.extend({
21
+ kind: z.literal('api/client'),
22
+ baseUrl: z.string().optional(),
23
+ timeout: z.number().int().positive().optional(),
24
+ debug: z.boolean().optional()
25
+ });
26
+
27
+ /**
28
+ * API Client implementation interface.
29
+ *
30
+ * Any runtime API client implementation must satisfy this contract.
31
+ */
32
+ export interface ApiClientImplementation {
33
+ /** Make a GET request */
34
+ get<T = unknown>(endpoint: string, options?: ApiRequestOptions): Promise<T>;
35
+ /** Make a POST request */
36
+ post<T = unknown>(endpoint: string, body?: unknown, options?: ApiRequestOptions): Promise<T>;
37
+ /** Make a PUT request */
38
+ put<T = unknown>(endpoint: string, body?: unknown, options?: ApiRequestOptions): Promise<T>;
39
+ /** Make a PATCH request */
40
+ patch<T = unknown>(endpoint: string, body?: unknown, options?: ApiRequestOptions): Promise<T>;
41
+ /** Make a DELETE request */
42
+ delete<T = unknown>(endpoint: string, options?: ApiRequestOptions): Promise<T>;
43
+ /** Make a generic request */
44
+ request<T = unknown>(
45
+ endpoint: string,
46
+ options?: ApiRequestOptions & { method?: string; body?: unknown }
47
+ ): Promise<T>;
48
+ /**
49
+ * Stream Server-Sent Events from an endpoint.
50
+ *
51
+ * Yields parsed SSE frames as they arrive. Each frame has already been
52
+ * split into event/data/id/retry fields and the data payload is
53
+ * JSON-parsed when possible. Returning from the generator — or the
54
+ * server closing the stream — ends iteration; an explicit `[DONE]`
55
+ * sentinel in the data stream also terminates.
56
+ *
57
+ * Use this instead of manually parsing SSE-wrapped POST bodies — the
58
+ * raw-fetch workaround (line-split + regex for `data: ...` lines) is
59
+ * brittle and fails on multi-line data, retry hints, and reconnects.
60
+ */
61
+ stream<T = unknown>(
62
+ endpoint: string,
63
+ options?: ApiStreamOptions
64
+ ): AsyncIterableIterator<ApiStreamEvent<T>>;
65
+ }
66
+
67
+ export interface ApiRequestOptions {
68
+ query?: Record<string, string | number | boolean | string[] | undefined>;
69
+ headers?: Record<string, string>;
70
+ timeout?: number;
71
+ params?: Record<string, string | number | boolean>;
72
+ }
73
+
74
+ export interface ApiStreamOptions extends ApiRequestOptions {
75
+ method?: string;
76
+ body?: unknown;
77
+ }
78
+
79
+ /**
80
+ * A single Server-Sent Event frame.
81
+ *
82
+ * `data` is JSON-parsed when the frame payload is valid JSON, otherwise
83
+ * the raw string is returned. `event`, `id`, and `retry` map to the
84
+ * corresponding SSE fields when present.
85
+ */
86
+ export interface ApiStreamEvent<T = unknown> {
87
+ event?: string;
88
+ data: T;
89
+ id?: string;
90
+ retry?: number;
91
+ }