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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +35 -29
- package/dist/cli.js +513 -22
- package/dist/commands/add.d.ts +33 -14
- package/dist/commands/add.js +222 -13
- package/dist/commands/compose.js +60 -23
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/index.js +4 -0
- package/dist/commands/init.js +23 -1
- package/dist/commands/migrate.d.ts +30 -0
- package/dist/commands/migrate.js +650 -0
- package/dist/commands/pipe.d.ts +1 -0
- package/dist/commands/pipe.js +31 -11
- package/dist/commands/remove.js +33 -2
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +37 -27
- package/dist/commands/search.d.ts +28 -0
- package/dist/commands/search.js +143 -0
- package/dist/commands/test.d.ts +65 -0
- package/dist/commands/test.js +454 -0
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +106 -14
- package/dist/commands/validate.d.ts +36 -0
- package/dist/commands/validate.js +97 -0
- package/dist/errors/index.d.ts +218 -0
- package/dist/errors/index.js +412 -0
- package/dist/mcp/server.js +84 -79
- package/dist/modules/composition.js +97 -32
- package/dist/modules/loader.js +4 -2
- package/dist/modules/runner.d.ts +65 -0
- package/dist/modules/runner.js +293 -49
- package/dist/modules/subagent.d.ts +6 -1
- package/dist/modules/subagent.js +18 -13
- package/dist/modules/validator.js +14 -6
- package/dist/providers/anthropic.d.ts +15 -0
- package/dist/providers/anthropic.js +147 -5
- package/dist/providers/base.d.ts +11 -0
- package/dist/providers/base.js +18 -0
- package/dist/providers/gemini.d.ts +15 -0
- package/dist/providers/gemini.js +122 -5
- package/dist/providers/ollama.d.ts +15 -0
- package/dist/providers/ollama.js +111 -3
- package/dist/providers/openai.d.ts +11 -0
- package/dist/providers/openai.js +133 -0
- package/dist/registry/client.d.ts +204 -0
- package/dist/registry/client.js +356 -0
- package/dist/registry/index.d.ts +4 -0
- package/dist/registry/index.js +4 -0
- package/dist/server/http.js +173 -42
- package/dist/types.d.ts +32 -1
- package/dist/types.js +4 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +31 -7
- package/dist/modules/composition.test.d.ts +0 -11
- package/dist/modules/composition.test.js +0 -450
- package/dist/modules/policy.test.d.ts +0 -10
- package/dist/modules/policy.test.js +0 -369
- package/src/cli.ts +0 -471
- package/src/commands/add.ts +0 -315
- package/src/commands/compose.ts +0 -185
- package/src/commands/index.ts +0 -13
- package/src/commands/init.ts +0 -94
- package/src/commands/list.ts +0 -33
- package/src/commands/pipe.ts +0 -76
- package/src/commands/remove.ts +0 -57
- package/src/commands/run.ts +0 -80
- package/src/commands/update.ts +0 -130
- package/src/commands/versions.ts +0 -79
- package/src/index.ts +0 -90
- package/src/mcp/index.ts +0 -5
- package/src/mcp/server.ts +0 -403
- package/src/modules/composition.test.ts +0 -558
- package/src/modules/composition.ts +0 -1674
- package/src/modules/index.ts +0 -9
- package/src/modules/loader.ts +0 -508
- package/src/modules/policy.test.ts +0 -455
- package/src/modules/runner.ts +0 -1983
- package/src/modules/subagent.ts +0 -277
- package/src/modules/validator.ts +0 -700
- package/src/providers/anthropic.ts +0 -89
- package/src/providers/base.ts +0 -29
- package/src/providers/deepseek.ts +0 -83
- package/src/providers/gemini.ts +0 -117
- package/src/providers/index.ts +0 -78
- package/src/providers/minimax.ts +0 -81
- package/src/providers/moonshot.ts +0 -82
- package/src/providers/ollama.ts +0 -83
- package/src/providers/openai.ts +0 -84
- package/src/providers/qwen.ts +0 -82
- package/src/server/http.ts +0 -316
- package/src/server/index.ts +0 -6
- package/src/types.ts +0 -599
- 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
|
+
}
|
package/dist/server/http.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
133
|
-
|
|
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(
|
|
226
|
+
const moduleData = await findModule(reqBody.module, searchPaths);
|
|
138
227
|
if (!moduleData) {
|
|
139
|
-
|
|
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(
|
|
145
|
-
|
|
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
|
-
|
|
241
|
+
args: reqBody.args,
|
|
148
242
|
useV22: true,
|
|
149
243
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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;
|