cognitive-modules-cli 2.2.5 → 2.2.7

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.
@@ -13,12 +13,13 @@
13
13
  import http from 'node:http';
14
14
  import { URL } from 'node:url';
15
15
  import { findModule, listModules, getDefaultSearchPaths } from '../modules/loader.js';
16
- import { runModule } from '../modules/runner.js';
16
+ import { runModule, runModuleStream } from '../modules/runner.js';
17
17
  import { getProvider } from '../providers/index.js';
18
18
  import { VERSION } from '../version.js';
19
- import { ErrorCodes, makeErrorEnvelope, makeHttpError } from '../errors/index.js';
19
+ import { ErrorCodes, attachContext, makeErrorEnvelope, makeHttpError, httpStatusForErrorCode } from '../errors/index.js';
20
+ import { encodeSseFrame } from './sse.js';
20
21
  // Supported protocol versions
21
- const SUPPORTED_VERSIONS = ['2.2', '2.1'];
22
+ const SUPPORTED_VERSIONS = ['2.2'];
22
23
  const DEFAULT_VERSION = '2.2';
23
24
  /**
24
25
  * Get requested protocol version from request
@@ -185,7 +186,9 @@ async function handleRun(req, res, searchPaths, url) {
185
186
  };
186
187
  // Verify API key
187
188
  if (!verifyApiKey(req)) {
188
- jsonResponse(res, 401, buildHttpError(ErrorCodes.PERMISSION_DENIED, 'Missing or invalid API Key', { suggestion: 'Use header: Authorization: Bearer <your-api-key>' }), protocolVersion);
189
+ const [status, body] = buildHttpError(ErrorCodes.PERMISSION_DENIED, 'Missing or invalid API Key', { suggestion: 'Use header: Authorization: Bearer <your-api-key>' });
190
+ // Auth failures are better represented as 401 even if the CEP code is permission-related.
191
+ jsonResponse(res, 401, body, protocolVersion);
189
192
  return;
190
193
  }
191
194
  // Parse request body
@@ -213,6 +216,13 @@ async function handleRun(req, res, searchPaths, url) {
213
216
  const reqBody = request;
214
217
  // Determine protocol version (body > header > query > default)
215
218
  protocolVersion = getRequestedVersion(req, url, reqBody.version);
219
+ // If the client explicitly requested an unsupported version, return a structured error.
220
+ const requested = reqBody.version ?? req.headers['x-cognitive-version'] ?? url.searchParams.get('version') ?? undefined;
221
+ if (requested && !SUPPORTED_VERSIONS.includes(requested)) {
222
+ const [status, body] = buildHttpError(ErrorCodes.UNSUPPORTED_VALUE, `Unsupported protocol version: ${requested}`, { suggestion: `Use version=${DEFAULT_VERSION}` });
223
+ jsonResponse(res, status, body, protocolVersion);
224
+ return;
225
+ }
216
226
  // Validate request
217
227
  if (!reqBody.module || !reqBody.args) {
218
228
  const [status, body] = buildHttpError(ErrorCodes.MISSING_REQUIRED_FIELD, 'Missing required fields: module, args', {
@@ -241,45 +251,18 @@ async function handleRun(req, res, searchPaths, url) {
241
251
  args: reqBody.args,
242
252
  useV22: true,
243
253
  });
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);
260
- }
261
- else {
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);
254
+ // Attach transport context but do not rebuild the envelope.
255
+ const contextual = attachContext(result, {
256
+ module: reqBody.module,
257
+ provider: providerName,
258
+ });
259
+ if (contextual.ok) {
260
+ jsonResponse(res, 200, contextual, protocolVersion);
261
+ return;
282
262
  }
263
+ const errorCode = (contextual.error?.code ?? ErrorCodes.INTERNAL_ERROR);
264
+ const status = httpStatusForErrorCode(errorCode);
265
+ jsonResponse(res, status, contextual, protocolVersion);
283
266
  }
284
267
  catch (error) {
285
268
  // Infrastructure error - still return envelope
@@ -292,6 +275,144 @@ async function handleRun(req, res, searchPaths, url) {
292
275
  jsonResponse(res, status, response, protocolVersion);
293
276
  }
294
277
  }
278
+ async function handleRunStream(req, res, searchPaths, url) {
279
+ let protocolVersion = DEFAULT_VERSION;
280
+ let sseStarted = false;
281
+ const beginSse = (version) => {
282
+ if (sseStarted)
283
+ return;
284
+ sseStarted = true;
285
+ res.writeHead(200, {
286
+ 'Content-Type': 'text/event-stream; charset=utf-8',
287
+ 'Cache-Control': 'no-cache, no-transform',
288
+ 'Connection': 'keep-alive',
289
+ 'X-Accel-Buffering': 'no',
290
+ 'Access-Control-Allow-Origin': '*',
291
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
292
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Cognitive-Version',
293
+ 'Access-Control-Expose-Headers': 'X-Cognitive-Version',
294
+ 'X-Cognitive-Version': version,
295
+ });
296
+ };
297
+ const writeEvent = (ev, id) => {
298
+ const type = typeof ev.type === 'string' ? ev.type : 'message';
299
+ res.write(encodeSseFrame(ev, { event: type, id }));
300
+ };
301
+ // Helper: send an error as CEP events (start + error + end).
302
+ const sendErrorStream = (envelope) => {
303
+ beginSse(protocolVersion);
304
+ let id = 1;
305
+ writeEvent({ type: 'start', version: protocolVersion, timestamp_ms: 0, module: envelope.module ?? 'unknown' }, id++);
306
+ const err = envelope.error ?? { code: ErrorCodes.INTERNAL_ERROR, message: 'Unknown error' };
307
+ writeEvent({ type: 'error', version: protocolVersion, timestamp_ms: 0, module: envelope.module ?? 'unknown', provider: envelope.provider, error: err }, id++);
308
+ writeEvent({ type: 'end', version: protocolVersion, timestamp_ms: 0, module: envelope.module ?? 'unknown', provider: envelope.provider, result: envelope }, id++);
309
+ res.end();
310
+ };
311
+ // Verify API key
312
+ if (!verifyApiKey(req)) {
313
+ // Auth failures should still be structured; SSE payload carries the error.
314
+ const envelope = attachContext(makeErrorEnvelope({
315
+ code: ErrorCodes.PERMISSION_DENIED,
316
+ message: 'Missing or invalid API Key',
317
+ suggestion: 'Use header: Authorization: Bearer <your-api-key>',
318
+ version: protocolVersion,
319
+ }), { module: 'unknown', provider: 'unknown' });
320
+ sendErrorStream(envelope);
321
+ return;
322
+ }
323
+ // Parse request body
324
+ let request;
325
+ try {
326
+ const body = await parseBody(req);
327
+ request = JSON.parse(body);
328
+ }
329
+ catch (e) {
330
+ const err = e;
331
+ const code = err?.code === 'PAYLOAD_TOO_LARGE' ? ErrorCodes.INPUT_TOO_LARGE : ErrorCodes.PARSE_ERROR;
332
+ const message = err?.code === 'PAYLOAD_TOO_LARGE' ? 'Payload too large' : 'Invalid JSON body';
333
+ const envelope = attachContext(makeErrorEnvelope({
334
+ code,
335
+ message,
336
+ suggestion: code === ErrorCodes.INPUT_TOO_LARGE ? 'Reduce input size to under 1MB' : 'Ensure request body is valid JSON',
337
+ version: protocolVersion,
338
+ }), { module: 'unknown', provider: 'unknown' });
339
+ sendErrorStream(envelope);
340
+ return;
341
+ }
342
+ if (!request || typeof request !== 'object') {
343
+ const envelope = attachContext(makeErrorEnvelope({
344
+ code: ErrorCodes.INVALID_INPUT,
345
+ message: 'Invalid request body',
346
+ suggestion: 'Ensure request body is a JSON object',
347
+ version: protocolVersion,
348
+ }), { module: 'unknown', provider: 'unknown' });
349
+ sendErrorStream(envelope);
350
+ return;
351
+ }
352
+ const reqBody = request;
353
+ protocolVersion = getRequestedVersion(req, url, reqBody.version);
354
+ // If the client explicitly requested an unsupported version, return a structured error.
355
+ const requested = reqBody.version ?? req.headers['x-cognitive-version'] ?? url.searchParams.get('version') ?? undefined;
356
+ if (requested && !SUPPORTED_VERSIONS.includes(requested)) {
357
+ const envelope = attachContext(makeErrorEnvelope({
358
+ code: ErrorCodes.UNSUPPORTED_VALUE,
359
+ message: `Unsupported protocol version: ${requested}`,
360
+ suggestion: `Use version=${DEFAULT_VERSION}`,
361
+ version: protocolVersion,
362
+ }), { module: reqBody?.module ?? 'unknown', provider: reqBody?.provider ?? 'unknown' });
363
+ sendErrorStream(envelope);
364
+ return;
365
+ }
366
+ // Validate request
367
+ if (!reqBody.module || !reqBody.args) {
368
+ const envelope = attachContext(makeErrorEnvelope({
369
+ code: ErrorCodes.MISSING_REQUIRED_FIELD,
370
+ message: 'Missing required fields: module, args',
371
+ suggestion: 'Provide both "module" and "args" fields in request body',
372
+ version: protocolVersion,
373
+ }), { module: reqBody?.module ?? 'unknown', provider: reqBody?.provider ?? 'unknown' });
374
+ sendErrorStream(envelope);
375
+ return;
376
+ }
377
+ // Find module
378
+ const moduleData = await findModule(reqBody.module, searchPaths);
379
+ if (!moduleData) {
380
+ const envelope = attachContext(makeErrorEnvelope({
381
+ code: ErrorCodes.MODULE_NOT_FOUND,
382
+ message: `Module '${reqBody.module}' not found`,
383
+ suggestion: 'Use GET /modules to list available modules',
384
+ version: protocolVersion,
385
+ }), { module: reqBody.module, provider: reqBody.provider ?? 'unknown' });
386
+ sendErrorStream(envelope);
387
+ return;
388
+ }
389
+ // Create provider
390
+ const provider = getProvider(reqBody.provider, reqBody.model);
391
+ const providerName = provider.name;
392
+ // Stream events
393
+ beginSse(protocolVersion);
394
+ let id = 1;
395
+ let closed = false;
396
+ const onClose = () => { closed = true; };
397
+ req.on('close', onClose);
398
+ res.on('close', onClose);
399
+ for await (const ev of runModuleStream(moduleData, provider, {
400
+ args: reqBody.args,
401
+ useV22: true,
402
+ })) {
403
+ if (closed)
404
+ break;
405
+ const contextualEv = {
406
+ ...ev,
407
+ module: reqBody.module,
408
+ provider: providerName,
409
+ };
410
+ writeEvent(contextualEv, id++);
411
+ }
412
+ if (!closed) {
413
+ res.end();
414
+ }
415
+ }
295
416
  export function createServer(options = {}) {
296
417
  const { cwd = process.cwd() } = options;
297
418
  const searchPaths = getDefaultSearchPaths(cwd);
@@ -328,6 +449,9 @@ export function createServer(options = {}) {
328
449
  else if (path === '/run' && method === 'POST') {
329
450
  await handleRun(req, res, searchPaths, url);
330
451
  }
452
+ else if (path === '/run/stream' && method === 'POST') {
453
+ await handleRunStream(req, res, searchPaths, url);
454
+ }
331
455
  else {
332
456
  const envelope = makeErrorEnvelope({
333
457
  code: ErrorCodes.ENDPOINT_NOT_FOUND,
@@ -364,6 +488,7 @@ export async function serve(options = {}) {
364
488
  console.log(' GET /modules - List modules');
365
489
  console.log(' GET /modules/:name - Module info');
366
490
  console.log(' POST /run - Run module');
491
+ console.log(' POST /run/stream - Run module (SSE stream)');
367
492
  resolve();
368
493
  });
369
494
  });
@@ -3,3 +3,5 @@
3
3
  */
4
4
  export { serve, createServer } from './http.js';
5
5
  export type { ServeOptions } from './http.js';
6
+ export { encodeSseFrame } from './sse.js';
7
+ export type { SseFrameOptions } from './sse.js';
@@ -2,3 +2,4 @@
2
2
  * Server - Re-export all server functionality
3
3
  */
4
4
  export { serve, createServer } from './http.js';
5
+ export { encodeSseFrame } from './sse.js';
@@ -0,0 +1,13 @@
1
+ export interface SseFrameOptions {
2
+ event?: string;
3
+ id?: string | number;
4
+ retryMs?: number;
5
+ }
6
+ /**
7
+ * Encode a payload into an SSE frame.
8
+ *
9
+ * Notes:
10
+ * - SSE requires each data line to be prefixed with `data:`.
11
+ * - `event:` maps cleanly to CEP `type` to keep transport mapping deterministic.
12
+ */
13
+ export declare function encodeSseFrame(data: unknown, options?: SseFrameOptions): string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Encode a payload into an SSE frame.
3
+ *
4
+ * Notes:
5
+ * - SSE requires each data line to be prefixed with `data:`.
6
+ * - `event:` maps cleanly to CEP `type` to keep transport mapping deterministic.
7
+ */
8
+ export function encodeSseFrame(data, options = {}) {
9
+ const lines = [];
10
+ if (options.retryMs !== undefined)
11
+ lines.push(`retry: ${options.retryMs}`);
12
+ if (options.id !== undefined)
13
+ lines.push(`id: ${options.id}`);
14
+ if (options.event)
15
+ lines.push(`event: ${options.event}`);
16
+ const json = JSON.stringify(data);
17
+ for (const line of json.split('\n')) {
18
+ lines.push(`data: ${line}`);
19
+ }
20
+ lines.push(''); // End of event
21
+ return lines.join('\n') + '\n';
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognitive-modules-cli",
3
- "version": "2.2.5",
3
+ "version": "2.2.7",
4
4
  "description": "Cognitive Modules - Structured AI Task Execution with version management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",