ctxpkg 0.0.1

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 (61) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +282 -0
  3. package/bin/cli.js +8 -0
  4. package/bin/daemon.js +7 -0
  5. package/package.json +70 -0
  6. package/src/agent/AGENTS.md +249 -0
  7. package/src/agent/agent.prompts.ts +66 -0
  8. package/src/agent/agent.test-runner.schemas.ts +158 -0
  9. package/src/agent/agent.test-runner.ts +436 -0
  10. package/src/agent/agent.ts +371 -0
  11. package/src/agent/agent.types.ts +94 -0
  12. package/src/backend/AGENTS.md +112 -0
  13. package/src/backend/backend.protocol.ts +95 -0
  14. package/src/backend/backend.schemas.ts +123 -0
  15. package/src/backend/backend.services.ts +151 -0
  16. package/src/backend/backend.ts +111 -0
  17. package/src/backend/backend.types.ts +34 -0
  18. package/src/cli/AGENTS.md +213 -0
  19. package/src/cli/cli.agent.ts +197 -0
  20. package/src/cli/cli.chat.ts +369 -0
  21. package/src/cli/cli.client.ts +55 -0
  22. package/src/cli/cli.collections.ts +491 -0
  23. package/src/cli/cli.config.ts +252 -0
  24. package/src/cli/cli.daemon.ts +160 -0
  25. package/src/cli/cli.documents.ts +413 -0
  26. package/src/cli/cli.mcp.ts +177 -0
  27. package/src/cli/cli.ts +28 -0
  28. package/src/cli/cli.utils.ts +122 -0
  29. package/src/client/AGENTS.md +135 -0
  30. package/src/client/client.adapters.ts +279 -0
  31. package/src/client/client.ts +86 -0
  32. package/src/client/client.types.ts +17 -0
  33. package/src/collections/AGENTS.md +185 -0
  34. package/src/collections/collections.schemas.ts +195 -0
  35. package/src/collections/collections.ts +1160 -0
  36. package/src/config/config.ts +118 -0
  37. package/src/daemon/AGENTS.md +168 -0
  38. package/src/daemon/daemon.config.ts +23 -0
  39. package/src/daemon/daemon.manager.ts +215 -0
  40. package/src/daemon/daemon.schemas.ts +22 -0
  41. package/src/daemon/daemon.ts +205 -0
  42. package/src/database/AGENTS.md +211 -0
  43. package/src/database/database.ts +64 -0
  44. package/src/database/migrations/migrations.001-init.ts +56 -0
  45. package/src/database/migrations/migrations.002-fts5.ts +32 -0
  46. package/src/database/migrations/migrations.ts +20 -0
  47. package/src/database/migrations/migrations.types.ts +9 -0
  48. package/src/documents/AGENTS.md +301 -0
  49. package/src/documents/documents.schemas.ts +190 -0
  50. package/src/documents/documents.ts +734 -0
  51. package/src/embedder/embedder.ts +53 -0
  52. package/src/exports.ts +0 -0
  53. package/src/mcp/AGENTS.md +264 -0
  54. package/src/mcp/mcp.ts +105 -0
  55. package/src/tools/AGENTS.md +228 -0
  56. package/src/tools/agent/agent.ts +45 -0
  57. package/src/tools/documents/documents.ts +401 -0
  58. package/src/tools/tools.langchain.ts +37 -0
  59. package/src/tools/tools.mcp.ts +46 -0
  60. package/src/tools/tools.types.ts +35 -0
  61. package/src/utils/utils.services.ts +46 -0
@@ -0,0 +1,371 @@
1
+ import { AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages';
2
+ import { createReactAgent } from '@langchain/langgraph/prebuilt';
3
+ import { ChatOpenAI } from '@langchain/openai';
4
+
5
+ import type {
6
+ AgentResponse,
7
+ AgentStep,
8
+ AgentStepCallback,
9
+ AskOptions,
10
+ DocumentAgentOptions,
11
+ LLMConfig,
12
+ RetryConfig,
13
+ } from './agent.types.ts';
14
+ import { AGENT_SYSTEM_PROMPT, formatCollectionRestriction, formatUserPrompt } from './agent.prompts.ts';
15
+
16
+ import type { BackendClient } from '#root/client/client.ts';
17
+ import { createDocumentToolDefinitions } from '#root/tools/documents/documents.ts';
18
+ import { toLangchainTools } from '#root/tools/tools.langchain.ts';
19
+
20
+ /** Default retry configuration */
21
+ const DEFAULT_RETRY_CONFIG: RetryConfig = {
22
+ maxRetries: 3,
23
+ initialDelayMs: 1000,
24
+ maxDelayMs: 30000,
25
+ backoffMultiplier: 2,
26
+ };
27
+
28
+ /**
29
+ * Sleep for a given number of milliseconds
30
+ */
31
+ const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
32
+
33
+ /**
34
+ * Check if an error is retryable (rate limit, temporary failure, etc.)
35
+ */
36
+ const isRetryableError = (error: unknown): boolean => {
37
+ if (error instanceof Error) {
38
+ const message = error.message.toLowerCase();
39
+ // Rate limit errors
40
+ if (message.includes('rate limit') || message.includes('429') || message.includes('too many requests')) {
41
+ return true;
42
+ }
43
+ // Temporary server errors
44
+ if (message.includes('500') || message.includes('502') || message.includes('503') || message.includes('504')) {
45
+ return true;
46
+ }
47
+ // Network errors
48
+ if (message.includes('econnreset') || message.includes('etimedout') || message.includes('network')) {
49
+ return true;
50
+ }
51
+ }
52
+ return false;
53
+ };
54
+
55
+ /**
56
+ * Execute a function with retry logic
57
+ */
58
+ const withRetry = async <T>(
59
+ fn: () => Promise<T>,
60
+ config: RetryConfig = DEFAULT_RETRY_CONFIG,
61
+ onRetry?: (attempt: number, error: Error, delayMs: number) => void,
62
+ ): Promise<T> => {
63
+ let lastError: Error | undefined;
64
+ let delayMs = config.initialDelayMs;
65
+
66
+ for (let attempt = 1; attempt <= config.maxRetries + 1; attempt++) {
67
+ try {
68
+ return await fn();
69
+ } catch (error) {
70
+ lastError = error instanceof Error ? error : new Error(String(error));
71
+
72
+ // Don't retry non-retryable errors or on last attempt
73
+ if (!isRetryableError(error) || attempt > config.maxRetries) {
74
+ throw lastError;
75
+ }
76
+
77
+ // Notify about retry
78
+ if (onRetry) {
79
+ onRetry(attempt, lastError, delayMs);
80
+ }
81
+
82
+ // Wait before retrying
83
+ await sleep(delayMs);
84
+
85
+ // Exponential backoff
86
+ delayMs = Math.min(delayMs * config.backoffMultiplier, config.maxDelayMs);
87
+ }
88
+ }
89
+
90
+ throw lastError;
91
+ };
92
+
93
+ /**
94
+ * Document search agent that uses LangChain tools to find and synthesize information.
95
+ */
96
+ class DocumentAgent {
97
+ #agent: ReturnType<typeof createReactAgent>;
98
+ #maxIterations: number;
99
+ #onStep?: AgentStepCallback;
100
+ #collections?: string[];
101
+ #conversationHistory: BaseMessage[];
102
+ #systemPrompt: string;
103
+
104
+ constructor(options: DocumentAgentOptions) {
105
+ const { llmConfig, tools, maxIterations = 15, onStep, collections } = options;
106
+
107
+ const llm = new ChatOpenAI({
108
+ configuration: {
109
+ baseURL: llmConfig.provider,
110
+ },
111
+ modelName: llmConfig.model,
112
+ apiKey: llmConfig.apiKey,
113
+ temperature: llmConfig.temperature,
114
+ maxTokens: llmConfig.maxTokens,
115
+ });
116
+
117
+ this.#agent = createReactAgent({
118
+ llm,
119
+ tools,
120
+ });
121
+
122
+ this.#maxIterations = maxIterations;
123
+ this.#onStep = onStep;
124
+ this.#collections = collections;
125
+ this.#conversationHistory = [];
126
+
127
+ // Build system prompt with collection restriction if needed
128
+ this.#systemPrompt = AGENT_SYSTEM_PROMPT;
129
+ if (collections && collections.length > 0) {
130
+ this.#systemPrompt += formatCollectionRestriction(collections);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Set the step callback for verbose mode
136
+ */
137
+ setOnStep(callback: AgentStepCallback | undefined): void {
138
+ this.#onStep = callback;
139
+ }
140
+
141
+ /**
142
+ * Clear conversation history for a fresh start
143
+ */
144
+ clearHistory(): void {
145
+ this.#conversationHistory = [];
146
+ }
147
+
148
+ /**
149
+ * Get current conversation history length
150
+ */
151
+ getHistoryLength(): number {
152
+ return this.#conversationHistory.length;
153
+ }
154
+
155
+ /**
156
+ * Ask a question and get a synthesized answer (stateless - doesn't use conversation history).
157
+ */
158
+ async ask(query: string, useCase: string, options?: AskOptions): Promise<AgentResponse> {
159
+ const onStep = options?.onStep ?? this.#onStep;
160
+ const userPrompt = formatUserPrompt(query, useCase, this.#collections);
161
+
162
+ const messages: BaseMessage[] = [new SystemMessage(this.#systemPrompt), new HumanMessage(userPrompt)];
163
+
164
+ return this.#runAgent(messages, onStep);
165
+ }
166
+
167
+ /**
168
+ * Chat with conversation history (stateful - maintains context across calls).
169
+ */
170
+ async chat(message: string, useCase: string, options?: AskOptions): Promise<AgentResponse> {
171
+ const onStep = options?.onStep ?? this.#onStep;
172
+
173
+ // Add user message to history
174
+ const userMessage = new HumanMessage(formatUserPrompt(message, useCase, this.#collections));
175
+ this.#conversationHistory.push(userMessage);
176
+
177
+ // Build full message list with system prompt
178
+ const messages: BaseMessage[] = [new SystemMessage(this.#systemPrompt), ...this.#conversationHistory];
179
+
180
+ const response = await this.#runAgent(messages, onStep);
181
+
182
+ // Add assistant response to history
183
+ this.#conversationHistory.push(new AIMessage(JSON.stringify(response)));
184
+
185
+ return response;
186
+ }
187
+
188
+ /**
189
+ * Run the agent with retry logic and step callbacks
190
+ */
191
+ async #runAgent(messages: BaseMessage[], onStep?: AgentStepCallback): Promise<AgentResponse> {
192
+ // Notify about starting
193
+ if (onStep) {
194
+ onStep({ type: 'thinking', content: 'Starting search...' });
195
+ }
196
+
197
+ const result = await withRetry(
198
+ async () => {
199
+ return this.#agent.invoke(
200
+ { messages },
201
+ {
202
+ recursionLimit: this.#maxIterations,
203
+ },
204
+ );
205
+ },
206
+ DEFAULT_RETRY_CONFIG,
207
+ (attempt, error, delayMs) => {
208
+ if (onStep) {
209
+ onStep({
210
+ type: 'error',
211
+ content: `Retry attempt ${attempt} after error: ${error.message}. Waiting ${delayMs}ms...`,
212
+ });
213
+ }
214
+ },
215
+ );
216
+
217
+ // Process messages for verbose output
218
+ if (onStep) {
219
+ this.#processMessagesForVerbose(result.messages, onStep);
220
+ }
221
+
222
+ // Extract the final message content
223
+ const resultMessages = result.messages;
224
+ const lastMessage = resultMessages[resultMessages.length - 1];
225
+ const content = typeof lastMessage.content === 'string' ? lastMessage.content : JSON.stringify(lastMessage.content);
226
+
227
+ // Try to parse as JSON response
228
+ return this.#parseResponse(content);
229
+ }
230
+
231
+ /**
232
+ * Process agent messages and emit verbose step callbacks
233
+ */
234
+ #processMessagesForVerbose(messages: BaseMessage[], onStep: AgentStepCallback): void {
235
+ for (const message of messages) {
236
+ if (message instanceof AIMessage) {
237
+ // Check for tool calls
238
+ const toolCalls = message.tool_calls;
239
+ if (toolCalls && toolCalls.length > 0) {
240
+ for (const toolCall of toolCalls) {
241
+ const step: AgentStep = {
242
+ type: 'tool_call',
243
+ content: `Calling ${toolCall.name}`,
244
+ toolName: toolCall.name,
245
+ toolInput: toolCall.args as Record<string, unknown>,
246
+ };
247
+ onStep(step);
248
+ }
249
+ } else if (message.content) {
250
+ // Regular AI message (thinking or final answer)
251
+ const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
252
+ if (content.trim()) {
253
+ onStep({ type: 'thinking', content: content.slice(0, 200) + (content.length > 200 ? '...' : '') });
254
+ }
255
+ }
256
+ } else if (message instanceof ToolMessage) {
257
+ // Tool result
258
+ const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
259
+ const preview = content.slice(0, 150) + (content.length > 150 ? '...' : '');
260
+ onStep({
261
+ type: 'tool_result',
262
+ content: preview,
263
+ toolName: message.name,
264
+ });
265
+ }
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Parse the agent's response, extracting JSON if present.
271
+ */
272
+ #parseResponse(content: string): AgentResponse {
273
+ // Try to find JSON in the response
274
+ const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/);
275
+ if (jsonMatch) {
276
+ try {
277
+ const parsed = JSON.parse(jsonMatch[1]);
278
+ return {
279
+ answer: parsed.answer ?? content,
280
+ sources: parsed.sources ?? [],
281
+ confidence: parsed.confidence ?? 'medium',
282
+ note: parsed.note,
283
+ };
284
+ } catch {
285
+ // Fall through to default
286
+ }
287
+ }
288
+
289
+ // Try to parse the whole content as JSON
290
+ try {
291
+ const parsed = JSON.parse(content);
292
+ if (parsed.answer) {
293
+ return {
294
+ answer: parsed.answer,
295
+ sources: parsed.sources ?? [],
296
+ confidence: parsed.confidence ?? 'medium',
297
+ note: parsed.note,
298
+ };
299
+ }
300
+ } catch {
301
+ // Fall through to default
302
+ }
303
+
304
+ // Default: treat the whole content as the answer
305
+ return {
306
+ answer: content,
307
+ sources: [],
308
+ confidence: 'medium',
309
+ };
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Options for creating a document agent
315
+ */
316
+ type CreateDocumentAgentOptions = {
317
+ /** Backend client for API calls */
318
+ client: BackendClient;
319
+ /** LLM configuration */
320
+ llmConfig: LLMConfig;
321
+ /** Optional map of alias names to collection IDs */
322
+ aliasMap?: Map<string, string>;
323
+ /** Maximum agent iterations */
324
+ maxIterations?: number;
325
+ /** Callback for verbose mode */
326
+ onStep?: AgentStepCallback;
327
+ /** Collections to restrict searches to */
328
+ collections?: string[];
329
+ };
330
+
331
+ /**
332
+ * Create a document search agent.
333
+ */
334
+ const createDocumentAgent = (options: CreateDocumentAgentOptions): DocumentAgent => {
335
+ const { client, llmConfig, aliasMap, maxIterations, onStep, collections } = options;
336
+
337
+ // Create document tool definitions and convert to LangChain tools
338
+ const toolDefinitions = createDocumentToolDefinitions({ client, aliasMap });
339
+ const langchainTools = toLangchainTools(toolDefinitions);
340
+ const tools = Object.values(langchainTools);
341
+
342
+ return new DocumentAgent({
343
+ llmConfig,
344
+ tools,
345
+ maxIterations,
346
+ onStep,
347
+ collections,
348
+ });
349
+ };
350
+
351
+ /**
352
+ * Get LLM config from the application config.
353
+ */
354
+ const getLLMConfigFromAppConfig = async (): Promise<LLMConfig> => {
355
+ const { config } = await import('#root/config/config.ts');
356
+
357
+ // Use type assertion for dynamic config access
358
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
359
+ const c = config as any;
360
+
361
+ return {
362
+ provider: c.get('llm.provider'),
363
+ model: c.get('llm.model'),
364
+ apiKey: c.get('llm.apiKey'),
365
+ temperature: c.get('llm.temperature'),
366
+ maxTokens: c.get('llm.maxTokens'),
367
+ };
368
+ };
369
+
370
+ export { DocumentAgent, createDocumentAgent, getLLMConfigFromAppConfig, withRetry, isRetryableError };
371
+ export type { CreateDocumentAgentOptions };
@@ -0,0 +1,94 @@
1
+ import * as z from 'zod';
2
+
3
+ /**
4
+ * LLM configuration for the agent
5
+ */
6
+ export type LLMConfig = {
7
+ /** OpenAI-compatible API base URL */
8
+ provider: string;
9
+ /** Model identifier */
10
+ model: string;
11
+ /** API key */
12
+ apiKey: string;
13
+ /** Temperature (0-2) */
14
+ temperature: number;
15
+ /** Maximum tokens */
16
+ maxTokens: number;
17
+ };
18
+
19
+ /**
20
+ * Source reference for an answer
21
+ */
22
+ export const sourceSchema = z.object({
23
+ collection: z.string(),
24
+ document: z.string(),
25
+ section: z.string().optional(),
26
+ });
27
+
28
+ export type Source = z.infer<typeof sourceSchema>;
29
+
30
+ /**
31
+ * Agent response
32
+ */
33
+ export const agentResponseSchema = z.object({
34
+ answer: z.string(),
35
+ sources: z.array(sourceSchema),
36
+ confidence: z.enum(['high', 'medium', 'low']),
37
+ note: z.string().optional(),
38
+ });
39
+
40
+ export type AgentResponse = z.infer<typeof agentResponseSchema>;
41
+
42
+ /**
43
+ * Callback for verbose mode - called when agent takes a step
44
+ */
45
+ export type AgentStepCallback = (step: AgentStep) => void;
46
+
47
+ /**
48
+ * Represents a step in the agent's reasoning
49
+ */
50
+ export type AgentStep = {
51
+ type: 'thinking' | 'tool_call' | 'tool_result' | 'error';
52
+ content: string;
53
+ toolName?: string;
54
+ toolInput?: Record<string, unknown>;
55
+ };
56
+
57
+ /**
58
+ * Options for creating a document agent
59
+ */
60
+ export type DocumentAgentOptions = {
61
+ /** LLM configuration */
62
+ llmConfig: LLMConfig;
63
+ /** LangChain tools to use */
64
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
+ tools: any[];
66
+ /** Maximum iterations before stopping */
67
+ maxIterations?: number;
68
+ /** Callback for verbose mode */
69
+ onStep?: AgentStepCallback;
70
+ /** Collections to restrict searches to (instruction in system prompt) */
71
+ collections?: string[];
72
+ };
73
+
74
+ /**
75
+ * Options for asking a question
76
+ */
77
+ export type AskOptions = {
78
+ /** Callback for verbose mode */
79
+ onStep?: AgentStepCallback;
80
+ };
81
+
82
+ /**
83
+ * Retry configuration for LLM calls
84
+ */
85
+ export type RetryConfig = {
86
+ /** Maximum number of retry attempts */
87
+ maxRetries: number;
88
+ /** Initial delay in ms */
89
+ initialDelayMs: number;
90
+ /** Maximum delay in ms */
91
+ maxDelayMs: number;
92
+ /** Multiplier for exponential backoff */
93
+ backoffMultiplier: number;
94
+ };
@@ -0,0 +1,112 @@
1
+ # Backend Service — Agent Guidelines
2
+
3
+ This document describes the backend service architecture for AI agents working on this module.
4
+
5
+ ## Overview
6
+
7
+ The backend service is a JSON-RPC 2.0 inspired request handler that provides the core API for ctxpkg. It exposes procedures for managing reference documents, collections, and system operations. The daemon runs this backend and exposes it via Unix socket.
8
+
9
+ ## File Structure
10
+
11
+ | File | Purpose |
12
+ |------|---------|
13
+ | `backend.ts` | Main `Backend` class — request routing and lifecycle |
14
+ | `backend.protocol.ts` | JSON-RPC protocol types, error codes, procedure helpers |
15
+ | `backend.schemas.ts` | Zod schemas for all API parameters and responses |
16
+ | `backend.services.ts` | Service procedure implementations (business logic) |
17
+ | `backend.types.ts` | Type utilities for type-safe client usage |
18
+
19
+ ## Architecture
20
+
21
+ ```
22
+ ┌─────────────────────────────────────────────────────────────┐
23
+ │ Backend │
24
+ │ ┌─────────────────┐ │
25
+ │ │ handleRequest() │ ← raw JSON │
26
+ │ └────────┬────────┘ │
27
+ │ │ parse & validate │
28
+ │ ┌────────▼────────┐ │
29
+ │ │ #routeRequest() │ → "service.method" dispatch │
30
+ │ └────────┬────────┘ │
31
+ │ │ │
32
+ │ ┌────────▼────────────────────────────────────────────┐ │
33
+ │ │ BackendServices │ │
34
+ │ │ ┌────────────┐ ┌─────────────┐ ┌────────────────┐ │ │
35
+ │ │ │ documents │ │ collections │ │ system │ │ │
36
+ │ │ └────────────┘ └─────────────┘ └────────────────┘ │ │
37
+ │ └─────────────────────────────────────────────────────┘ │
38
+ └─────────────────────────────────────────────────────────────┘
39
+ ```
40
+
41
+ ### Request Flow
42
+
43
+ 1. Raw JSON arrives at `handleRequest()`
44
+ 2. Request is parsed and validated against `requestSchema`
45
+ 3. Method string (`"service.method"`) is split and routed
46
+ 4. Procedure input is validated against its Zod schema
47
+ 5. Handler executes and returns result or error
48
+
49
+ ### Protocol Format
50
+
51
+ ```typescript
52
+ // Request: { id, method: "service.method", params? }
53
+ // Success: { id, result }
54
+ // Error: { id, error: { code, message, data? } }
55
+ ```
56
+
57
+ Error codes are defined in `ErrorCodes` (e.g., `ParseError`, `MethodNotFound`, `InvalidParams`, `ServiceError`).
58
+
59
+ ## Adding New Procedures
60
+
61
+ 1. **Add schema** in `backend.schemas.ts`:
62
+
63
+ ```typescript
64
+ const myNewParamsSchema = z.object({
65
+ foo: z.string(),
66
+ bar: z.number().optional(),
67
+ });
68
+ ```
69
+
70
+ 2. **Add procedure** in `backend.services.ts` under the appropriate service namespace:
71
+
72
+ ```typescript
73
+ const myService = {
74
+ myMethod: procedure(myNewParamsSchema, async (params): Promise<MyResult> => {
75
+ const service = services.get(MyService);
76
+ return service.doSomething(params);
77
+ }),
78
+ };
79
+ ```
80
+
81
+ 3. **Export types** if needed from `backend.schemas.ts`.
82
+
83
+ ## Key Patterns
84
+
85
+ ### Procedure Definition
86
+
87
+ Use the `procedure()` helper for type-safe handlers:
88
+
89
+ ```typescript
90
+ procedure(
91
+ inputSchema, // Zod schema for params
92
+ async (params) => { // Handler receives validated params
93
+ return result; // Return type is inferred
94
+ }
95
+ );
96
+ ```
97
+
98
+ ### Service Access
99
+
100
+ Services are accessed via the dependency injection container:
101
+
102
+ ```typescript
103
+ const docService = services.get(DocumentsService);
104
+ ```
105
+
106
+ ### Method Routing
107
+
108
+ Methods use `service.method` format — the Backend class splits this and looks up the procedure in `BackendServices`.
109
+
110
+ ### Type-Safe Clients
111
+
112
+ The `BackendAPI` type in `backend.types.ts` converts procedures to function signatures. Use `GetBackendAPIParams` and `GetBackendAPIResponse` helpers to extract types for specific methods.
@@ -0,0 +1,95 @@
1
+ import { z } from 'zod';
2
+
3
+ // Request/Response protocol (JSON-RPC 2.0 inspired)
4
+ const requestSchema = z.object({
5
+ id: z.string(),
6
+ method: z.string(),
7
+ params: z.unknown().optional(),
8
+ });
9
+
10
+ type Request = z.infer<typeof requestSchema>;
11
+
12
+ const responseSchema = z.object({
13
+ id: z.string(),
14
+ result: z.unknown().optional(),
15
+ error: z
16
+ .object({
17
+ code: z.number(),
18
+ message: z.string(),
19
+ data: z.unknown().optional(),
20
+ })
21
+ .optional(),
22
+ });
23
+
24
+ type Response = z.infer<typeof responseSchema>;
25
+
26
+ // Standard error codes
27
+ const ErrorCodes = {
28
+ ParseError: -32700,
29
+ InvalidRequest: -32600,
30
+ MethodNotFound: -32601,
31
+ InvalidParams: -32602,
32
+ InternalError: -32603,
33
+ // Custom codes
34
+ ServiceError: -32000,
35
+ NotConnected: -32001,
36
+ Timeout: -32002,
37
+ } as const;
38
+
39
+ type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
40
+
41
+ // Procedure definition for type-safe handlers
42
+ type Procedure<TInput extends z.ZodTypeAny, TOutput> = {
43
+ input: TInput;
44
+ handler: (params: z.infer<TInput>) => Promise<TOutput>;
45
+ };
46
+
47
+ // Helper to create procedures with type inference
48
+ const procedure = <TInput extends z.ZodTypeAny, TOutput>(
49
+ input: TInput,
50
+ handler: (params: z.infer<TInput>) => Promise<TOutput>,
51
+ ): Procedure<TInput, TOutput> => ({
52
+ input,
53
+ handler,
54
+ });
55
+
56
+ // Service definition type - maps method names to procedures
57
+ type ServiceDefinition = Record<string, Procedure<z.ZodTypeAny, unknown>>;
58
+
59
+ // Extract input type from a procedure
60
+ type ProcedureInput<T> = T extends Procedure<infer TInput, unknown> ? z.infer<TInput> : never;
61
+
62
+ // Extract output type from a procedure
63
+ type ProcedureOutput<T> = T extends Procedure<z.ZodTypeAny, infer TOutput> ? TOutput : never;
64
+
65
+ // Convert a procedure to its function signature
66
+ // When input is an empty object, the parameter becomes optional
67
+ type ProcedureToFunction<T> =
68
+ T extends Procedure<infer TInput, infer TOutput>
69
+ ? keyof z.infer<TInput> extends never
70
+ ? (params?: z.infer<TInput>) => Promise<TOutput>
71
+ : (params: z.infer<TInput>) => Promise<TOutput>
72
+ : never;
73
+
74
+ // Create response helpers
75
+ const createSuccessResponse = (id: string, result: unknown): Response => ({
76
+ id,
77
+ result,
78
+ });
79
+
80
+ const createErrorResponse = (id: string, code: ErrorCode, message: string, data?: unknown): Response => ({
81
+ id,
82
+ error: { code, message, data },
83
+ });
84
+
85
+ export type {
86
+ Request,
87
+ Response,
88
+ Procedure,
89
+ ServiceDefinition,
90
+ ProcedureInput,
91
+ ProcedureOutput,
92
+ ProcedureToFunction,
93
+ ErrorCode,
94
+ };
95
+ export { requestSchema, responseSchema, ErrorCodes, procedure, createSuccessResponse, createErrorResponse };