cognitive-modules-cli 2.2.1 → 2.2.5

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 (95) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -29
  4. package/dist/cli.js +513 -22
  5. package/dist/commands/add.d.ts +33 -14
  6. package/dist/commands/add.js +222 -13
  7. package/dist/commands/compose.js +60 -23
  8. package/dist/commands/index.d.ts +4 -0
  9. package/dist/commands/index.js +4 -0
  10. package/dist/commands/init.js +23 -1
  11. package/dist/commands/migrate.d.ts +30 -0
  12. package/dist/commands/migrate.js +650 -0
  13. package/dist/commands/pipe.d.ts +1 -0
  14. package/dist/commands/pipe.js +31 -11
  15. package/dist/commands/remove.js +33 -2
  16. package/dist/commands/run.d.ts +1 -0
  17. package/dist/commands/run.js +37 -27
  18. package/dist/commands/search.d.ts +28 -0
  19. package/dist/commands/search.js +143 -0
  20. package/dist/commands/test.d.ts +65 -0
  21. package/dist/commands/test.js +454 -0
  22. package/dist/commands/update.d.ts +1 -0
  23. package/dist/commands/update.js +106 -14
  24. package/dist/commands/validate.d.ts +36 -0
  25. package/dist/commands/validate.js +97 -0
  26. package/dist/errors/index.d.ts +218 -0
  27. package/dist/errors/index.js +412 -0
  28. package/dist/mcp/server.js +84 -79
  29. package/dist/modules/composition.js +97 -32
  30. package/dist/modules/loader.js +4 -2
  31. package/dist/modules/runner.d.ts +65 -0
  32. package/dist/modules/runner.js +293 -49
  33. package/dist/modules/subagent.d.ts +6 -1
  34. package/dist/modules/subagent.js +18 -13
  35. package/dist/modules/validator.js +14 -6
  36. package/dist/providers/anthropic.d.ts +15 -0
  37. package/dist/providers/anthropic.js +147 -5
  38. package/dist/providers/base.d.ts +11 -0
  39. package/dist/providers/base.js +18 -0
  40. package/dist/providers/gemini.d.ts +15 -0
  41. package/dist/providers/gemini.js +122 -5
  42. package/dist/providers/ollama.d.ts +15 -0
  43. package/dist/providers/ollama.js +111 -3
  44. package/dist/providers/openai.d.ts +11 -0
  45. package/dist/providers/openai.js +133 -0
  46. package/dist/registry/client.d.ts +204 -0
  47. package/dist/registry/client.js +356 -0
  48. package/dist/registry/index.d.ts +4 -0
  49. package/dist/registry/index.js +4 -0
  50. package/dist/server/http.js +173 -42
  51. package/dist/types.d.ts +32 -1
  52. package/dist/types.js +4 -1
  53. package/dist/version.d.ts +1 -0
  54. package/dist/version.js +4 -0
  55. package/package.json +31 -7
  56. package/dist/modules/composition.test.d.ts +0 -11
  57. package/dist/modules/composition.test.js +0 -450
  58. package/dist/modules/policy.test.d.ts +0 -10
  59. package/dist/modules/policy.test.js +0 -369
  60. package/src/cli.ts +0 -471
  61. package/src/commands/add.ts +0 -315
  62. package/src/commands/compose.ts +0 -185
  63. package/src/commands/index.ts +0 -13
  64. package/src/commands/init.ts +0 -94
  65. package/src/commands/list.ts +0 -33
  66. package/src/commands/pipe.ts +0 -76
  67. package/src/commands/remove.ts +0 -57
  68. package/src/commands/run.ts +0 -80
  69. package/src/commands/update.ts +0 -130
  70. package/src/commands/versions.ts +0 -79
  71. package/src/index.ts +0 -90
  72. package/src/mcp/index.ts +0 -5
  73. package/src/mcp/server.ts +0 -403
  74. package/src/modules/composition.test.ts +0 -558
  75. package/src/modules/composition.ts +0 -1674
  76. package/src/modules/index.ts +0 -9
  77. package/src/modules/loader.ts +0 -508
  78. package/src/modules/policy.test.ts +0 -455
  79. package/src/modules/runner.ts +0 -1983
  80. package/src/modules/subagent.ts +0 -277
  81. package/src/modules/validator.ts +0 -700
  82. package/src/providers/anthropic.ts +0 -89
  83. package/src/providers/base.ts +0 -29
  84. package/src/providers/deepseek.ts +0 -83
  85. package/src/providers/gemini.ts +0 -117
  86. package/src/providers/index.ts +0 -78
  87. package/src/providers/minimax.ts +0 -81
  88. package/src/providers/moonshot.ts +0 -82
  89. package/src/providers/ollama.ts +0 -83
  90. package/src/providers/openai.ts +0 -84
  91. package/src/providers/qwen.ts +0 -82
  92. package/src/server/http.ts +0 -316
  93. package/src/server/index.ts +0 -6
  94. package/src/types.ts +0 -599
  95. package/tsconfig.json +0 -17
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Registry Client - Fetch and manage modules from Cognitive Modules Registry
3
+ *
4
+ * Supports both v1 and v2 registry formats.
5
+ *
6
+ * Usage:
7
+ * const client = new RegistryClient();
8
+ * const modules = await client.listModules();
9
+ * const module = await client.getModule('code-reviewer');
10
+ */
11
+ import { existsSync, statSync } from 'node:fs';
12
+ import { writeFile, readFile, mkdir } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { homedir } from 'node:os';
15
+ import { createHash } from 'node:crypto';
16
+ // =============================================================================
17
+ // Constants
18
+ // =============================================================================
19
+ const DEFAULT_REGISTRY_URL = 'https://raw.githubusercontent.com/ziel-io/cognitive-modules/main/cognitive-registry.json';
20
+ const CACHE_DIR = join(homedir(), '.cognitive', 'cache');
21
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
22
+ const REGISTRY_FETCH_TIMEOUT_MS = 10_000; // 10s
23
+ const MAX_REGISTRY_BYTES = 1024 * 1024; // 1MB
24
+ // =============================================================================
25
+ // Registry Client
26
+ // =============================================================================
27
+ export class RegistryClient {
28
+ registryUrl;
29
+ cache = { data: null, timestamp: 0 };
30
+ constructor(registryUrl = DEFAULT_REGISTRY_URL) {
31
+ this.registryUrl = registryUrl;
32
+ }
33
+ async parseRegistryResponse(response) {
34
+ const contentLengthHeader = response.headers?.get('content-length');
35
+ if (contentLengthHeader) {
36
+ const contentLength = Number(contentLengthHeader);
37
+ if (!Number.isNaN(contentLength) && contentLength > MAX_REGISTRY_BYTES) {
38
+ throw new Error(`Registry payload too large: ${contentLength} bytes (max ${MAX_REGISTRY_BYTES})`);
39
+ }
40
+ }
41
+ if (response.body && typeof response.body.getReader === 'function') {
42
+ const reader = response.body.getReader();
43
+ const decoder = new TextDecoder('utf-8');
44
+ let buffer = '';
45
+ let totalBytes = 0;
46
+ try {
47
+ while (true) {
48
+ const { done, value } = await reader.read();
49
+ if (done)
50
+ break;
51
+ if (value) {
52
+ totalBytes += value.byteLength;
53
+ if (totalBytes > MAX_REGISTRY_BYTES) {
54
+ throw new Error(`Registry payload too large: ${totalBytes} bytes (max ${MAX_REGISTRY_BYTES})`);
55
+ }
56
+ buffer += decoder.decode(value, { stream: true });
57
+ }
58
+ }
59
+ buffer += decoder.decode();
60
+ }
61
+ finally {
62
+ reader.releaseLock();
63
+ }
64
+ try {
65
+ return JSON.parse(buffer);
66
+ }
67
+ catch (error) {
68
+ throw new Error(`Invalid registry JSON: ${error.message}`);
69
+ }
70
+ }
71
+ if (typeof response.text === 'function') {
72
+ const text = await response.text();
73
+ const byteLen = Buffer.byteLength(text, 'utf-8');
74
+ if (byteLen > MAX_REGISTRY_BYTES) {
75
+ throw new Error(`Registry payload too large: ${byteLen} bytes (max ${MAX_REGISTRY_BYTES})`);
76
+ }
77
+ try {
78
+ return JSON.parse(text);
79
+ }
80
+ catch (error) {
81
+ throw new Error(`Invalid registry JSON: ${error.message}`);
82
+ }
83
+ }
84
+ if (typeof response.json === 'function') {
85
+ return await response.json();
86
+ }
87
+ throw new Error('Failed to read registry response body');
88
+ }
89
+ /**
90
+ * Generate a unique cache filename based on registry URL
91
+ */
92
+ getCacheFileName() {
93
+ const hash = createHash('md5').update(this.registryUrl).digest('hex').slice(0, 8);
94
+ return `registry-${hash}.json`;
95
+ }
96
+ /**
97
+ * Fetch registry index (with caching)
98
+ */
99
+ async fetchRegistry(forceRefresh = false) {
100
+ const now = Date.now();
101
+ // Check memory cache
102
+ if (!forceRefresh && this.cache.data && (now - this.cache.timestamp) < CACHE_TTL_MS) {
103
+ return this.cache.data;
104
+ }
105
+ // Check file cache (unique per registry URL)
106
+ const cacheFile = join(CACHE_DIR, this.getCacheFileName());
107
+ if (!forceRefresh && existsSync(cacheFile)) {
108
+ try {
109
+ const stat = statSync(cacheFile);
110
+ if ((now - stat.mtimeMs) < CACHE_TTL_MS) {
111
+ const content = await readFile(cacheFile, 'utf-8');
112
+ const data = JSON.parse(content);
113
+ this.cache = { data, timestamp: now };
114
+ return data;
115
+ }
116
+ }
117
+ catch {
118
+ // Ignore cache read errors
119
+ }
120
+ }
121
+ // Fetch from network
122
+ const controller = new AbortController();
123
+ const timeout = setTimeout(() => controller.abort(), REGISTRY_FETCH_TIMEOUT_MS);
124
+ let data;
125
+ try {
126
+ const response = await fetch(this.registryUrl, {
127
+ headers: { 'User-Agent': 'cognitive-runtime/2.2' },
128
+ signal: controller.signal,
129
+ });
130
+ if (!response.ok) {
131
+ throw new Error(`Failed to fetch registry: ${response.status} ${response.statusText}`);
132
+ }
133
+ data = await this.parseRegistryResponse(response);
134
+ }
135
+ catch (error) {
136
+ if (error instanceof Error && error.name === 'AbortError') {
137
+ throw new Error(`Registry fetch timed out after ${REGISTRY_FETCH_TIMEOUT_MS}ms`);
138
+ }
139
+ throw error;
140
+ }
141
+ finally {
142
+ clearTimeout(timeout);
143
+ }
144
+ // Update cache
145
+ this.cache = { data, timestamp: now };
146
+ // Save to file cache
147
+ try {
148
+ await mkdir(CACHE_DIR, { recursive: true });
149
+ await writeFile(cacheFile, JSON.stringify(data, null, 2));
150
+ }
151
+ catch {
152
+ // Ignore cache write errors
153
+ }
154
+ return data;
155
+ }
156
+ /**
157
+ * Check if registry is v2 format
158
+ */
159
+ isV2Registry(registry) {
160
+ const firstModule = Object.values(registry.modules)[0];
161
+ return firstModule && 'identity' in firstModule;
162
+ }
163
+ /**
164
+ * Normalize module entry to unified format
165
+ */
166
+ normalizeModule(name, entry) {
167
+ if ('identity' in entry) {
168
+ // v2 format
169
+ const v2 = entry;
170
+ return {
171
+ name: v2.identity.name,
172
+ version: v2.identity.version,
173
+ description: v2.metadata.description,
174
+ author: v2.metadata.author,
175
+ source: v2.distribution.source || v2.distribution.tarball || '',
176
+ keywords: v2.metadata.keywords || [],
177
+ tier: v2.metadata.tier,
178
+ namespace: v2.identity.namespace,
179
+ license: v2.metadata.license,
180
+ repository: v2.metadata.repository,
181
+ conformance_level: v2.quality?.conformance_level,
182
+ verified: v2.quality?.verified,
183
+ deprecated: v2.quality?.deprecated,
184
+ };
185
+ }
186
+ else {
187
+ // v1 format
188
+ const v1 = entry;
189
+ return {
190
+ name,
191
+ version: v1.version,
192
+ description: v1.description,
193
+ author: v1.author,
194
+ source: v1.source,
195
+ keywords: v1.tags || [],
196
+ };
197
+ }
198
+ }
199
+ /**
200
+ * List all modules in registry
201
+ */
202
+ async listModules() {
203
+ const registry = await this.fetchRegistry();
204
+ return Object.entries(registry.modules).map(([name, entry]) => this.normalizeModule(name, entry));
205
+ }
206
+ /**
207
+ * Get a specific module by name
208
+ */
209
+ async getModule(name) {
210
+ const registry = await this.fetchRegistry();
211
+ const entry = registry.modules[name];
212
+ if (!entry) {
213
+ return null;
214
+ }
215
+ return this.normalizeModule(name, entry);
216
+ }
217
+ /**
218
+ * Search modules by query
219
+ */
220
+ async search(query) {
221
+ const modules = await this.listModules();
222
+ // If query is empty, return all modules sorted by name
223
+ if (!query.trim()) {
224
+ return modules
225
+ .map(m => ({
226
+ name: m.name,
227
+ description: m.description,
228
+ version: m.version,
229
+ score: 1,
230
+ keywords: m.keywords,
231
+ }))
232
+ .sort((a, b) => a.name.localeCompare(b.name));
233
+ }
234
+ const queryLower = query.toLowerCase().trim();
235
+ const queryTerms = queryLower.split(/\s+/).filter(t => t.length > 0);
236
+ const results = [];
237
+ for (const module of modules) {
238
+ let score = 0;
239
+ // Name match (highest weight)
240
+ if (module.name.toLowerCase().includes(queryLower)) {
241
+ score += 10;
242
+ if (module.name.toLowerCase() === queryLower) {
243
+ score += 5;
244
+ }
245
+ }
246
+ // Description match
247
+ const descLower = module.description.toLowerCase();
248
+ for (const term of queryTerms) {
249
+ if (descLower.includes(term)) {
250
+ score += 3;
251
+ }
252
+ }
253
+ // Keyword match
254
+ for (const keyword of module.keywords) {
255
+ const keywordLower = keyword.toLowerCase();
256
+ for (const term of queryTerms) {
257
+ if (keywordLower.includes(term) || term.includes(keywordLower)) {
258
+ score += 2;
259
+ }
260
+ }
261
+ }
262
+ if (score > 0) {
263
+ results.push({
264
+ name: module.name,
265
+ description: module.description,
266
+ version: module.version,
267
+ score,
268
+ keywords: module.keywords,
269
+ });
270
+ }
271
+ }
272
+ // Sort by score descending
273
+ results.sort((a, b) => b.score - a.score);
274
+ return results;
275
+ }
276
+ /**
277
+ * Get categories
278
+ */
279
+ async getCategories() {
280
+ const registry = await this.fetchRegistry();
281
+ return registry.categories || {};
282
+ }
283
+ /**
284
+ * Parse GitHub source string
285
+ * Format: github:<owner>/<repo>[/<path>][@<ref>]
286
+ */
287
+ parseGitHubSource(source) {
288
+ if (!source.startsWith('github:')) {
289
+ return null;
290
+ }
291
+ const rest = source.slice('github:'.length);
292
+ // Split ref if present
293
+ const [pathPart, ref] = rest.split('@');
294
+ // Parse owner/repo/path
295
+ const parts = pathPart.split('/');
296
+ if (parts.length < 2) {
297
+ return null;
298
+ }
299
+ const org = parts[0];
300
+ const repo = parts[1];
301
+ const modulePath = parts.length > 2 ? parts.slice(2).join('/') : undefined;
302
+ return { org, repo, path: modulePath, ref };
303
+ }
304
+ /**
305
+ * Verify checksum of downloaded file
306
+ */
307
+ async verifyChecksum(filePath, expected) {
308
+ const [algo, expectedHash] = expected.split(':');
309
+ if (!algo || !expectedHash) {
310
+ throw new Error(`Invalid checksum format: ${expected}`);
311
+ }
312
+ const content = await readFile(filePath);
313
+ const actualHash = createHash(algo).update(content).digest('hex');
314
+ return actualHash === expectedHash;
315
+ }
316
+ /**
317
+ * Get the download URL for a module
318
+ */
319
+ async getDownloadUrl(moduleName) {
320
+ const module = await this.getModule(moduleName);
321
+ if (!module) {
322
+ throw new Error(`Module not found in registry: ${moduleName}`);
323
+ }
324
+ const source = module.source;
325
+ // Check if it's a GitHub source
326
+ const githubInfo = this.parseGitHubSource(source);
327
+ if (githubInfo) {
328
+ return {
329
+ url: `https://github.com/${githubInfo.org}/${githubInfo.repo}`,
330
+ isGitHub: true,
331
+ githubInfo,
332
+ };
333
+ }
334
+ // Check if it's a tarball URL
335
+ if (source.startsWith('http://') || source.startsWith('https://')) {
336
+ return {
337
+ url: source,
338
+ isGitHub: false,
339
+ };
340
+ }
341
+ throw new Error(`Unknown source format: ${source}`);
342
+ }
343
+ }
344
+ // =============================================================================
345
+ // Exports
346
+ // =============================================================================
347
+ export const defaultRegistry = new RegistryClient();
348
+ export async function listRegistryModules() {
349
+ return defaultRegistry.listModules();
350
+ }
351
+ export async function getRegistryModule(name) {
352
+ return defaultRegistry.getModule(name);
353
+ }
354
+ export async function searchRegistry(query) {
355
+ return defaultRegistry.search(query);
356
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Registry Module - Re-export all registry functionality
3
+ */
4
+ export * from './client.js';
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Registry Module - Re-export all registry functionality
3
+ */
4
+ export * from './client.js';
@@ -15,22 +15,62 @@ import { URL } from 'node:url';
15
15
  import { findModule, listModules, getDefaultSearchPaths } from '../modules/loader.js';
16
16
  import { runModule } from '../modules/runner.js';
17
17
  import { getProvider } from '../providers/index.js';
18
+ import { VERSION } from '../version.js';
19
+ import { ErrorCodes, makeErrorEnvelope, makeHttpError } from '../errors/index.js';
20
+ // Supported protocol versions
21
+ const SUPPORTED_VERSIONS = ['2.2', '2.1'];
22
+ const DEFAULT_VERSION = '2.2';
23
+ /**
24
+ * Get requested protocol version from request
25
+ * Priority: body.version > X-Cognitive-Version header > query param > default
26
+ */
27
+ function getRequestedVersion(req, url, bodyVersion) {
28
+ // Body version takes priority
29
+ if (bodyVersion && SUPPORTED_VERSIONS.includes(bodyVersion)) {
30
+ return bodyVersion;
31
+ }
32
+ // Check header
33
+ const headerVersion = req.headers['x-cognitive-version'];
34
+ if (headerVersion && SUPPORTED_VERSIONS.includes(headerVersion)) {
35
+ return headerVersion;
36
+ }
37
+ // Check query param
38
+ const queryVersion = url.searchParams.get('version');
39
+ if (queryVersion && SUPPORTED_VERSIONS.includes(queryVersion)) {
40
+ return queryVersion;
41
+ }
42
+ return DEFAULT_VERSION;
43
+ }
18
44
  // =============================================================================
19
45
  // Helpers
20
46
  // =============================================================================
21
- function jsonResponse(res, status, data) {
47
+ function jsonResponse(res, status, data, protocolVersion = DEFAULT_VERSION) {
22
48
  res.writeHead(status, {
23
49
  'Content-Type': 'application/json',
24
50
  'Access-Control-Allow-Origin': '*',
25
51
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
26
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
52
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Cognitive-Version',
53
+ 'X-Cognitive-Version': protocolVersion,
27
54
  });
28
55
  res.end(JSON.stringify(data, null, 2));
29
56
  }
30
- function parseBody(req) {
57
+ const MAX_BODY_BYTES = 1024 * 1024; // 1MB
58
+ function parseBody(req, maxBytes = MAX_BODY_BYTES) {
31
59
  return new Promise((resolve, reject) => {
32
60
  let body = '';
33
- req.on('data', (chunk) => (body += chunk));
61
+ let received = 0;
62
+ req.on('data', (chunk) => {
63
+ const chunkSize = typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length;
64
+ received += chunkSize;
65
+ if (received > maxBytes) {
66
+ const err = new Error('Payload too large');
67
+ err.code = 'PAYLOAD_TOO_LARGE';
68
+ req.destroy(err);
69
+ reject(err);
70
+ return;
71
+ }
72
+ body += chunk;
73
+ });
34
74
  req.on('end', () => resolve(body));
35
75
  req.on('error', reject);
36
76
  });
@@ -51,7 +91,16 @@ function verifyApiKey(req) {
51
91
  async function handleRoot(res) {
52
92
  jsonResponse(res, 200, {
53
93
  name: 'Cognitive Modules API',
54
- version: '1.3.0',
94
+ version: VERSION,
95
+ protocol: {
96
+ version: DEFAULT_VERSION,
97
+ supported: SUPPORTED_VERSIONS,
98
+ negotiation: {
99
+ header: 'X-Cognitive-Version',
100
+ query: '?version=2.2',
101
+ body: 'version field in request body',
102
+ },
103
+ },
55
104
  docs: '/docs',
56
105
  endpoints: {
57
106
  run: 'POST /run',
@@ -72,7 +121,7 @@ async function handleHealth(res) {
72
121
  };
73
122
  jsonResponse(res, 200, {
74
123
  status: 'healthy',
75
- version: '1.3.0',
124
+ version: VERSION,
76
125
  providers,
77
126
  });
78
127
  }
@@ -95,7 +144,12 @@ async function handleModules(res, searchPaths) {
95
144
  async function handleModuleInfo(res, moduleName, searchPaths) {
96
145
  const moduleData = await findModule(moduleName, searchPaths);
97
146
  if (!moduleData) {
98
- jsonResponse(res, 404, { error: `Module '${moduleName}' not found` });
147
+ const envelope = makeErrorEnvelope({
148
+ code: ErrorCodes.MODULE_NOT_FOUND,
149
+ message: `Module '${moduleName}' not found`,
150
+ suggestion: 'Use GET /modules to list available modules',
151
+ });
152
+ jsonResponse(res, 404, envelope);
99
153
  return;
100
154
  }
101
155
  jsonResponse(res, 200, {
@@ -110,12 +164,28 @@ async function handleModuleInfo(res, moduleName, searchPaths) {
110
164
  outputSchema: moduleData.outputSchema,
111
165
  });
112
166
  }
113
- async function handleRun(req, res, searchPaths) {
167
+ async function handleRun(req, res, searchPaths, url) {
168
+ // Version will be determined after parsing body
169
+ let protocolVersion = DEFAULT_VERSION;
170
+ // Helper to build error envelope using unified error factory
171
+ const buildHttpError = (code, message, options = {}) => {
172
+ const moduleName = options.moduleName ?? request?.module ?? 'unknown';
173
+ const providerName = options.providerName ?? request?.provider ?? 'unknown';
174
+ const [status, envelope] = makeHttpError({
175
+ code,
176
+ message,
177
+ version: protocolVersion,
178
+ suggestion: options.suggestion,
179
+ recoverable: options.recoverable,
180
+ retry_after_ms: options.retry_after_ms,
181
+ module: moduleName,
182
+ provider: providerName,
183
+ });
184
+ return [status, envelope];
185
+ };
114
186
  // Verify API key
115
187
  if (!verifyApiKey(req)) {
116
- jsonResponse(res, 401, {
117
- error: 'Missing or invalid API Key. Use header: Authorization: Bearer <your-api-key>',
118
- });
188
+ jsonResponse(res, 401, buildHttpError(ErrorCodes.PERMISSION_DENIED, 'Missing or invalid API Key', { suggestion: 'Use header: Authorization: Bearer <your-api-key>' }), protocolVersion);
119
189
  return;
120
190
  }
121
191
  // Parse request body
@@ -124,52 +194,102 @@ async function handleRun(req, res, searchPaths) {
124
194
  const body = await parseBody(req);
125
195
  request = JSON.parse(body);
126
196
  }
127
- catch {
128
- jsonResponse(res, 400, { error: 'Invalid JSON body' });
197
+ catch (e) {
198
+ const err = e;
199
+ if (err?.code === 'PAYLOAD_TOO_LARGE') {
200
+ const [status, body] = buildHttpError(ErrorCodes.INPUT_TOO_LARGE, 'Payload too large', { suggestion: 'Reduce input size to under 1MB' });
201
+ jsonResponse(res, status, body, protocolVersion);
202
+ return;
203
+ }
204
+ const [status, body] = buildHttpError(ErrorCodes.PARSE_ERROR, 'Invalid JSON body', { suggestion: 'Ensure request body is valid JSON' });
205
+ jsonResponse(res, status, body, protocolVersion);
129
206
  return;
130
207
  }
208
+ if (!request || typeof request !== 'object') {
209
+ const [status, body] = buildHttpError(ErrorCodes.INVALID_INPUT, 'Invalid request body', { suggestion: 'Ensure request body is a JSON object' });
210
+ jsonResponse(res, status, body, protocolVersion);
211
+ return;
212
+ }
213
+ const reqBody = request;
214
+ // Determine protocol version (body > header > query > default)
215
+ protocolVersion = getRequestedVersion(req, url, reqBody.version);
131
216
  // Validate request
132
- if (!request.module || !request.args) {
133
- jsonResponse(res, 400, { error: 'Missing required fields: module, args' });
217
+ if (!reqBody.module || !reqBody.args) {
218
+ const [status, body] = buildHttpError(ErrorCodes.MISSING_REQUIRED_FIELD, 'Missing required fields: module, args', {
219
+ moduleName: reqBody?.module ?? 'unknown',
220
+ suggestion: 'Provide both "module" and "args" fields in request body'
221
+ });
222
+ jsonResponse(res, status, body, protocolVersion);
134
223
  return;
135
224
  }
136
225
  // Find module
137
- const moduleData = await findModule(request.module, searchPaths);
226
+ const moduleData = await findModule(reqBody.module, searchPaths);
138
227
  if (!moduleData) {
139
- jsonResponse(res, 404, { error: `Module '${request.module}' not found` });
228
+ const [status, body] = buildHttpError(ErrorCodes.MODULE_NOT_FOUND, `Module '${reqBody.module}' not found`, {
229
+ moduleName: reqBody.module,
230
+ suggestion: 'Use GET /modules to list available modules'
231
+ });
232
+ jsonResponse(res, status, body, protocolVersion);
140
233
  return;
141
234
  }
142
235
  try {
143
236
  // Create provider
144
- const provider = getProvider(request.provider, request.model);
145
- // Run module
237
+ const provider = getProvider(reqBody.provider, reqBody.model);
238
+ const providerName = provider.name;
239
+ // Run module (always use v2.2 format internally)
146
240
  const result = await runModule(moduleData, provider, {
147
- input: { query: request.args, code: request.args },
241
+ args: reqBody.args,
148
242
  useV22: true,
149
243
  });
150
- const response = {
151
- ok: result.ok,
152
- module: request.module,
153
- provider: request.provider || process.env.LLM_PROVIDER || 'openai',
154
- };
155
- if (result.ok) {
156
- if ('meta' in result)
157
- response.meta = result.meta;
158
- if ('data' in result)
159
- response.data = result.data;
244
+ // Build response envelope with requested protocol version
245
+ if (result.ok && 'data' in result) {
246
+ const v22Result = result;
247
+ const response = {
248
+ ok: true,
249
+ version: protocolVersion,
250
+ meta: v22Result.meta ?? {
251
+ confidence: 0.5,
252
+ risk: 'medium',
253
+ explain: 'No meta provided',
254
+ },
255
+ data: v22Result.data,
256
+ module: reqBody.module,
257
+ provider: providerName,
258
+ };
259
+ jsonResponse(res, 200, response, protocolVersion);
160
260
  }
161
261
  else {
162
- if ('error' in result)
163
- response.error = result.error?.message;
262
+ // Error response - must include meta and full error object
263
+ const errorResult = result;
264
+ const response = {
265
+ ok: false,
266
+ version: protocolVersion,
267
+ meta: errorResult.meta ?? {
268
+ confidence: 0.0,
269
+ risk: 'high',
270
+ explain: errorResult.error?.message ?? 'An error occurred',
271
+ },
272
+ error: errorResult.error ?? {
273
+ code: ErrorCodes.INTERNAL_ERROR,
274
+ message: 'Unknown error',
275
+ recoverable: false,
276
+ },
277
+ partial_data: errorResult.partial_data,
278
+ module: reqBody.module,
279
+ provider: providerName,
280
+ };
281
+ jsonResponse(res, 200, response, protocolVersion);
164
282
  }
165
- jsonResponse(res, 200, response);
166
283
  }
167
284
  catch (error) {
168
- jsonResponse(res, 500, {
169
- ok: false,
170
- error: error instanceof Error ? error.message : String(error),
171
- module: request.module,
285
+ // Infrastructure error - still return envelope
286
+ const errorMessage = error instanceof Error ? error.message : String(error);
287
+ const [status, response] = buildHttpError(ErrorCodes.INTERNAL_ERROR, errorMessage, {
288
+ moduleName: reqBody?.module,
289
+ providerName: reqBody?.provider,
290
+ recoverable: false,
172
291
  });
292
+ jsonResponse(res, status, response, protocolVersion);
173
293
  }
174
294
  }
175
295
  export function createServer(options = {}) {
@@ -184,7 +304,8 @@ export function createServer(options = {}) {
184
304
  res.writeHead(204, {
185
305
  'Access-Control-Allow-Origin': '*',
186
306
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
187
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
307
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Cognitive-Version',
308
+ 'Access-Control-Expose-Headers': 'X-Cognitive-Version',
188
309
  });
189
310
  res.end();
190
311
  return;
@@ -205,17 +326,27 @@ export function createServer(options = {}) {
205
326
  await handleModuleInfo(res, moduleName, searchPaths);
206
327
  }
207
328
  else if (path === '/run' && method === 'POST') {
208
- await handleRun(req, res, searchPaths);
329
+ await handleRun(req, res, searchPaths, url);
209
330
  }
210
331
  else {
211
- jsonResponse(res, 404, { error: 'Not found' });
332
+ const envelope = makeErrorEnvelope({
333
+ code: ErrorCodes.ENDPOINT_NOT_FOUND,
334
+ message: `Endpoint '${path}' not found`,
335
+ suggestion: 'Use GET / to see available endpoints',
336
+ risk: 'low',
337
+ });
338
+ jsonResponse(res, 404, envelope);
212
339
  }
213
340
  }
214
341
  catch (error) {
215
342
  console.error('Server error:', error);
216
- jsonResponse(res, 500, {
217
- error: error instanceof Error ? error.message : 'Internal server error',
343
+ const errorMessage = error instanceof Error ? error.message : 'Internal server error';
344
+ const envelope = makeErrorEnvelope({
345
+ code: ErrorCodes.INTERNAL_ERROR,
346
+ message: errorMessage,
347
+ recoverable: false,
218
348
  });
349
+ jsonResponse(res, 500, envelope);
219
350
  }
220
351
  });
221
352
  return server;