cognitive-modules-cli 2.2.1 → 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.
Files changed (101) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -29
  4. package/dist/cli.js +519 -23
  5. package/dist/commands/add.d.ts +33 -14
  6. package/dist/commands/add.js +383 -16
  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 +2 -0
  17. package/dist/commands/run.js +61 -28
  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 +225 -0
  27. package/dist/errors/index.js +420 -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 +72 -5
  32. package/dist/modules/runner.js +306 -59
  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 +212 -0
  47. package/dist/registry/client.js +359 -0
  48. package/dist/registry/index.d.ts +4 -0
  49. package/dist/registry/index.js +4 -0
  50. package/dist/registry/tar.d.ts +8 -0
  51. package/dist/registry/tar.js +353 -0
  52. package/dist/server/http.js +301 -45
  53. package/dist/server/index.d.ts +2 -0
  54. package/dist/server/index.js +1 -0
  55. package/dist/server/sse.d.ts +13 -0
  56. package/dist/server/sse.js +22 -0
  57. package/dist/types.d.ts +32 -1
  58. package/dist/types.js +4 -1
  59. package/dist/version.d.ts +1 -0
  60. package/dist/version.js +4 -0
  61. package/package.json +31 -7
  62. package/dist/modules/composition.test.d.ts +0 -11
  63. package/dist/modules/composition.test.js +0 -450
  64. package/dist/modules/policy.test.d.ts +0 -10
  65. package/dist/modules/policy.test.js +0 -369
  66. package/src/cli.ts +0 -471
  67. package/src/commands/add.ts +0 -315
  68. package/src/commands/compose.ts +0 -185
  69. package/src/commands/index.ts +0 -13
  70. package/src/commands/init.ts +0 -94
  71. package/src/commands/list.ts +0 -33
  72. package/src/commands/pipe.ts +0 -76
  73. package/src/commands/remove.ts +0 -57
  74. package/src/commands/run.ts +0 -80
  75. package/src/commands/update.ts +0 -130
  76. package/src/commands/versions.ts +0 -79
  77. package/src/index.ts +0 -90
  78. package/src/mcp/index.ts +0 -5
  79. package/src/mcp/server.ts +0 -403
  80. package/src/modules/composition.test.ts +0 -558
  81. package/src/modules/composition.ts +0 -1674
  82. package/src/modules/index.ts +0 -9
  83. package/src/modules/loader.ts +0 -508
  84. package/src/modules/policy.test.ts +0 -455
  85. package/src/modules/runner.ts +0 -1983
  86. package/src/modules/subagent.ts +0 -277
  87. package/src/modules/validator.ts +0 -700
  88. package/src/providers/anthropic.ts +0 -89
  89. package/src/providers/base.ts +0 -29
  90. package/src/providers/deepseek.ts +0 -83
  91. package/src/providers/gemini.ts +0 -117
  92. package/src/providers/index.ts +0 -78
  93. package/src/providers/minimax.ts +0 -81
  94. package/src/providers/moonshot.ts +0 -82
  95. package/src/providers/ollama.ts +0 -83
  96. package/src/providers/openai.ts +0 -84
  97. package/src/providers/qwen.ts +0 -82
  98. package/src/server/http.ts +0 -316
  99. package/src/server/index.ts +0 -6
  100. package/src/types.ts +0 -599
  101. package/tsconfig.json +0 -17
@@ -0,0 +1,454 @@
1
+ /**
2
+ * Test Command - Run golden tests for Cognitive Modules
3
+ *
4
+ * Usage:
5
+ * cog test <module> - Run tests for a specific module
6
+ * cog test --all - Run tests for all modules
7
+ * cog test <module> --update - Update expected outputs
8
+ *
9
+ * Tests are defined in module.yaml:
10
+ * tests:
11
+ * - tests/case1.input.json -> tests/case1.expected.json
12
+ */
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
15
+ import { findModule, listModules, getDefaultSearchPaths } from '../modules/loader.js';
16
+ import { runModule } from '../modules/runner.js';
17
+ // =============================================================================
18
+ // Test Case Parsing
19
+ // =============================================================================
20
+ /**
21
+ * Parse test definitions from module.yaml tests field
22
+ *
23
+ * Format: "tests/case1.input.json -> tests/case1.expected.json"
24
+ */
25
+ export function parseTestDefinitions(module, modulePath) {
26
+ const tests = [];
27
+ const testsConfig = module.tests;
28
+ if (!testsConfig || !Array.isArray(testsConfig)) {
29
+ return tests;
30
+ }
31
+ for (const testDef of testsConfig) {
32
+ // Parse "input.json -> expected.json" format
33
+ const match = testDef.match(/^\s*(.+?)\s*->\s*(.+?)\s*$/);
34
+ if (!match) {
35
+ continue;
36
+ }
37
+ const inputRelPath = match[1];
38
+ const expectedRelPath = match[2];
39
+ // Get the module directory
40
+ const moduleDir = path.dirname(modulePath);
41
+ const inputPath = path.resolve(moduleDir, inputRelPath);
42
+ const expectedPath = path.resolve(moduleDir, expectedRelPath);
43
+ // Extract test name from input file
44
+ const testName = path.basename(inputRelPath, '.input.json');
45
+ tests.push({
46
+ name: testName,
47
+ inputPath,
48
+ expectedPath,
49
+ });
50
+ }
51
+ return tests;
52
+ }
53
+ /**
54
+ * Auto-discover test cases in tests/ directory
55
+ */
56
+ export function discoverTestCases(modulePath) {
57
+ const tests = [];
58
+ const moduleDir = path.dirname(modulePath);
59
+ const testsDir = path.join(moduleDir, 'tests');
60
+ if (!fs.existsSync(testsDir)) {
61
+ return tests;
62
+ }
63
+ const files = fs.readdirSync(testsDir);
64
+ const inputFiles = files.filter(f => f.endsWith('.input.json'));
65
+ for (const inputFile of inputFiles) {
66
+ const testName = inputFile.replace('.input.json', '');
67
+ const expectedFile = `${testName}.expected.json`;
68
+ if (files.includes(expectedFile)) {
69
+ tests.push({
70
+ name: testName,
71
+ inputPath: path.join(testsDir, inputFile),
72
+ expectedPath: path.join(testsDir, expectedFile),
73
+ });
74
+ }
75
+ }
76
+ return tests;
77
+ }
78
+ // =============================================================================
79
+ // Test Execution
80
+ // =============================================================================
81
+ /**
82
+ * Check if expected file uses schema validation format ($validate)
83
+ */
84
+ function isSchemaValidationFormat(expected) {
85
+ return (typeof expected === 'object' &&
86
+ expected !== null &&
87
+ '$validate' in expected);
88
+ }
89
+ /**
90
+ * Validate actual result against schema validation format
91
+ * Returns array of validation errors
92
+ */
93
+ function validateAgainstSchema(schema, actual, path = '') {
94
+ const errors = [];
95
+ if (actual === null || actual === undefined) {
96
+ errors.push({ field: path || 'root', expected: 'value', actual });
97
+ return errors;
98
+ }
99
+ // Handle const constraint
100
+ if ('const' in schema) {
101
+ if (actual !== schema.const) {
102
+ errors.push({ field: path || 'root', expected: `const: ${schema.const}`, actual });
103
+ }
104
+ return errors;
105
+ }
106
+ // Handle type constraint
107
+ if ('type' in schema) {
108
+ const schemaType = schema.type;
109
+ const actualType = Array.isArray(actual) ? 'array' : typeof actual;
110
+ if (schemaType === 'array' && !Array.isArray(actual)) {
111
+ errors.push({ field: path || 'root', expected: 'array', actual: actualType });
112
+ return errors;
113
+ }
114
+ if (schemaType === 'object' && (typeof actual !== 'object' || Array.isArray(actual))) {
115
+ errors.push({ field: path || 'root', expected: 'object', actual: actualType });
116
+ return errors;
117
+ }
118
+ if (schemaType === 'number' && typeof actual !== 'number') {
119
+ errors.push({ field: path || 'root', expected: 'number', actual: actualType });
120
+ return errors;
121
+ }
122
+ if (schemaType === 'string' && typeof actual !== 'string') {
123
+ errors.push({ field: path || 'root', expected: 'string', actual: actualType });
124
+ return errors;
125
+ }
126
+ }
127
+ // Handle number constraints
128
+ if (typeof actual === 'number') {
129
+ if ('minimum' in schema && actual < schema.minimum) {
130
+ errors.push({ field: path, expected: `>= ${schema.minimum}`, actual });
131
+ }
132
+ if ('maximum' in schema && actual > schema.maximum) {
133
+ errors.push({ field: path, expected: `<= ${schema.maximum}`, actual });
134
+ }
135
+ }
136
+ // Handle string constraints
137
+ if (typeof actual === 'string') {
138
+ if ('minLength' in schema && actual.length < schema.minLength) {
139
+ errors.push({ field: path, expected: `minLength: ${schema.minLength}`, actual: `length: ${actual.length}` });
140
+ }
141
+ if ('maxLength' in schema && actual.length > schema.maxLength) {
142
+ errors.push({ field: path, expected: `maxLength: ${schema.maxLength}`, actual: `length: ${actual.length}` });
143
+ }
144
+ if ('enum' in schema && !(schema.enum.includes(actual))) {
145
+ errors.push({ field: path, expected: `enum: ${JSON.stringify(schema.enum)}`, actual });
146
+ }
147
+ }
148
+ // Handle array constraints
149
+ if (Array.isArray(actual)) {
150
+ if ('minItems' in schema && actual.length < schema.minItems) {
151
+ errors.push({ field: path, expected: `minItems: ${schema.minItems}`, actual: `length: ${actual.length}` });
152
+ }
153
+ if ('items' in schema) {
154
+ const itemSchema = schema.items;
155
+ for (let i = 0; i < actual.length; i++) {
156
+ errors.push(...validateAgainstSchema(itemSchema, actual[i], `${path}[${i}]`));
157
+ }
158
+ }
159
+ }
160
+ // Handle object constraints
161
+ if (typeof actual === 'object' && !Array.isArray(actual) && actual !== null) {
162
+ const actualObj = actual;
163
+ // Check required fields
164
+ if ('required' in schema) {
165
+ const required = schema.required;
166
+ for (const field of required) {
167
+ if (!(field in actualObj)) {
168
+ errors.push({ field: `${path}.${field}`, expected: 'required field', actual: 'missing' });
169
+ }
170
+ }
171
+ }
172
+ // Validate properties
173
+ if ('properties' in schema) {
174
+ const properties = schema.properties;
175
+ for (const [key, propSchema] of Object.entries(properties)) {
176
+ if (key in actualObj) {
177
+ errors.push(...validateAgainstSchema(propSchema, actualObj[key], path ? `${path}.${key}` : key));
178
+ }
179
+ }
180
+ }
181
+ }
182
+ return errors;
183
+ }
184
+ /**
185
+ * Compare two values and return differences
186
+ */
187
+ function deepCompare(expected, actual, path = '') {
188
+ const diffs = [];
189
+ // Handle null/undefined
190
+ if (expected === null || expected === undefined) {
191
+ if (actual !== expected) {
192
+ diffs.push({ field: path || 'root', expected, actual });
193
+ }
194
+ return diffs;
195
+ }
196
+ // Type mismatch
197
+ if (typeof expected !== typeof actual) {
198
+ diffs.push({ field: path || 'root', expected, actual });
199
+ return diffs;
200
+ }
201
+ // Arrays
202
+ if (Array.isArray(expected)) {
203
+ if (!Array.isArray(actual)) {
204
+ diffs.push({ field: path || 'root', expected, actual });
205
+ return diffs;
206
+ }
207
+ if (expected.length !== actual.length) {
208
+ diffs.push({ field: `${path}.length`, expected: expected.length, actual: actual.length });
209
+ }
210
+ const maxLen = Math.max(expected.length, actual.length);
211
+ for (let i = 0; i < maxLen; i++) {
212
+ diffs.push(...deepCompare(expected[i], actual[i], `${path}[${i}]`));
213
+ }
214
+ return diffs;
215
+ }
216
+ // Objects
217
+ if (typeof expected === 'object') {
218
+ if (typeof actual !== 'object' || actual === null) {
219
+ diffs.push({ field: path || 'root', expected, actual });
220
+ return diffs;
221
+ }
222
+ const expectedObj = expected;
223
+ const actualObj = actual;
224
+ const allKeys = new Set([...Object.keys(expectedObj), ...Object.keys(actualObj)]);
225
+ for (const key of allKeys) {
226
+ // Skip dynamic fields that change between runs
227
+ if (['trace_id', 'latency_ms', 'model', 'timestamp', 'version'].includes(key)) {
228
+ continue;
229
+ }
230
+ const fieldPath = path ? `${path}.${key}` : key;
231
+ if (!(key in expectedObj)) {
232
+ // Extra field in actual - only report if it's significant
233
+ continue;
234
+ }
235
+ if (!(key in actualObj)) {
236
+ diffs.push({ field: fieldPath, expected: expectedObj[key], actual: undefined });
237
+ continue;
238
+ }
239
+ diffs.push(...deepCompare(expectedObj[key], actualObj[key], fieldPath));
240
+ }
241
+ return diffs;
242
+ }
243
+ // Primitives
244
+ if (expected !== actual) {
245
+ // For confidence, allow small variations
246
+ if (path.endsWith('.confidence') && typeof expected === 'number' && typeof actual === 'number') {
247
+ if (Math.abs(expected - actual) <= 0.1) {
248
+ return diffs; // Close enough
249
+ }
250
+ }
251
+ diffs.push({ field: path || 'root', expected, actual });
252
+ }
253
+ return diffs;
254
+ }
255
+ /**
256
+ * Run a single test case
257
+ */
258
+ async function runTestCase(testCase, module, ctx, options = {}) {
259
+ const startTime = Date.now();
260
+ const { timeout = 60000 } = options;
261
+ try {
262
+ // Load input
263
+ if (!fs.existsSync(testCase.inputPath)) {
264
+ return {
265
+ name: testCase.name,
266
+ passed: false,
267
+ duration_ms: Date.now() - startTime,
268
+ error: `Input file not found: ${testCase.inputPath}`,
269
+ };
270
+ }
271
+ const inputContent = fs.readFileSync(testCase.inputPath, 'utf-8');
272
+ const input = JSON.parse(inputContent);
273
+ // Load expected (if exists)
274
+ let expected = null;
275
+ if (fs.existsSync(testCase.expectedPath)) {
276
+ const expectedContent = fs.readFileSync(testCase.expectedPath, 'utf-8');
277
+ expected = JSON.parse(expectedContent);
278
+ }
279
+ // Run module with timeout
280
+ const runPromise = runModule(module, ctx.provider, {
281
+ input,
282
+ validateInput: true,
283
+ validateOutput: true,
284
+ useV22: true,
285
+ });
286
+ const timeoutPromise = new Promise((_, reject) => {
287
+ setTimeout(() => reject(new Error(`Test timeout after ${timeout}ms`)), timeout);
288
+ });
289
+ const result = await Promise.race([runPromise, timeoutPromise]);
290
+ // Update mode - write actual output as expected
291
+ if (options.update) {
292
+ fs.writeFileSync(testCase.expectedPath, JSON.stringify(result, null, 2));
293
+ return {
294
+ name: testCase.name,
295
+ passed: true,
296
+ duration_ms: Date.now() - startTime,
297
+ };
298
+ }
299
+ // No expected file - skip comparison
300
+ if (expected === null) {
301
+ return {
302
+ name: testCase.name,
303
+ passed: false,
304
+ duration_ms: Date.now() - startTime,
305
+ error: `Expected file not found: ${testCase.expectedPath}`,
306
+ };
307
+ }
308
+ // Compare results - use schema validation or direct comparison
309
+ let diffs;
310
+ if (isSchemaValidationFormat(expected)) {
311
+ // Schema validation mode
312
+ diffs = validateAgainstSchema(expected.$validate, result);
313
+ }
314
+ else {
315
+ // Direct comparison mode
316
+ diffs = deepCompare(expected, result);
317
+ }
318
+ if (diffs.length === 0) {
319
+ return {
320
+ name: testCase.name,
321
+ passed: true,
322
+ duration_ms: Date.now() - startTime,
323
+ };
324
+ }
325
+ else {
326
+ return {
327
+ name: testCase.name,
328
+ passed: false,
329
+ duration_ms: Date.now() - startTime,
330
+ diff: diffs.slice(0, 10), // Limit to first 10 diffs
331
+ error: `${diffs.length} difference(s) found`,
332
+ };
333
+ }
334
+ }
335
+ catch (error) {
336
+ return {
337
+ name: testCase.name,
338
+ passed: false,
339
+ duration_ms: Date.now() - startTime,
340
+ error: error instanceof Error ? error.message : String(error),
341
+ };
342
+ }
343
+ }
344
+ // =============================================================================
345
+ // Commands
346
+ // =============================================================================
347
+ /**
348
+ * Run tests for a single module
349
+ */
350
+ export async function test(moduleName, ctx, options = {}) {
351
+ const startTime = Date.now();
352
+ const searchPaths = getDefaultSearchPaths(ctx.cwd);
353
+ // Find module
354
+ const module = await findModule(moduleName, searchPaths);
355
+ if (!module) {
356
+ return {
357
+ success: false,
358
+ error: `Module '${moduleName}' not found`,
359
+ };
360
+ }
361
+ // Get test cases from module.yaml or auto-discover
362
+ let testCases = parseTestDefinitions(module, module.location);
363
+ if (testCases.length === 0) {
364
+ testCases = discoverTestCases(module.location);
365
+ }
366
+ if (testCases.length === 0) {
367
+ return {
368
+ success: true,
369
+ data: {
370
+ moduleName: module.name,
371
+ modulePath: module.location,
372
+ total: 0,
373
+ passed: 0,
374
+ failed: 0,
375
+ skipped: 0,
376
+ duration_ms: Date.now() - startTime,
377
+ results: [],
378
+ },
379
+ };
380
+ }
381
+ // Filter tests if specified
382
+ if (options.filter) {
383
+ testCases = testCases.filter(tc => tc.name.includes(options.filter));
384
+ }
385
+ // Run tests
386
+ const results = [];
387
+ let passed = 0;
388
+ let failed = 0;
389
+ for (const testCase of testCases) {
390
+ if (options.verbose) {
391
+ console.error(` Running: ${testCase.name}...`);
392
+ }
393
+ const result = await runTestCase(testCase, module, ctx, options);
394
+ results.push(result);
395
+ if (result.passed) {
396
+ passed++;
397
+ }
398
+ else {
399
+ failed++;
400
+ }
401
+ }
402
+ const testResult = {
403
+ moduleName: module.name,
404
+ modulePath: module.location,
405
+ total: testCases.length,
406
+ passed,
407
+ failed,
408
+ skipped: 0,
409
+ duration_ms: Date.now() - startTime,
410
+ results,
411
+ };
412
+ return {
413
+ success: failed === 0,
414
+ data: testResult,
415
+ };
416
+ }
417
+ /**
418
+ * Run tests for all modules
419
+ */
420
+ export async function testAll(ctx, options = {}) {
421
+ const startTime = Date.now();
422
+ const searchPaths = getDefaultSearchPaths(ctx.cwd);
423
+ // List all modules
424
+ const modules = await listModules(searchPaths);
425
+ const results = [];
426
+ let totalTests = 0;
427
+ let totalPassed = 0;
428
+ let totalFailed = 0;
429
+ let modulesWithTests = 0;
430
+ for (const moduleInfo of modules) {
431
+ const result = await test(moduleInfo.name, ctx, options);
432
+ if (result.success && result.data) {
433
+ const moduleResult = result.data;
434
+ results.push(moduleResult);
435
+ if (moduleResult.total > 0) {
436
+ modulesWithTests++;
437
+ totalTests += moduleResult.total;
438
+ totalPassed += moduleResult.passed;
439
+ totalFailed += moduleResult.failed;
440
+ }
441
+ }
442
+ }
443
+ return {
444
+ success: totalFailed === 0,
445
+ data: {
446
+ total: totalTests,
447
+ passed: totalPassed,
448
+ failed: totalFailed,
449
+ skipped: modules.length - modulesWithTests,
450
+ duration_ms: Date.now() - startTime,
451
+ modules: results,
452
+ },
453
+ };
454
+ }
@@ -7,6 +7,7 @@
7
7
  import type { CommandContext, CommandResult } from '../types.js';
8
8
  export interface UpdateOptions {
9
9
  tag?: string;
10
+ registry?: string;
10
11
  }
11
12
  /**
12
13
  * Update an installed module
@@ -6,15 +6,38 @@
6
6
  */
7
7
  import { existsSync } from 'node:fs';
8
8
  import { readFile } from 'node:fs/promises';
9
- import { join } from 'node:path';
9
+ import { join, resolve, sep, isAbsolute } from 'node:path';
10
10
  import { homedir } from 'node:os';
11
- import { add, getInstallInfo } from './add.js';
11
+ import { add, addFromRegistry, getInstallInfo } from './add.js';
12
+ import { RegistryClient } from '../registry/client.js';
12
13
  const USER_MODULES_DIR = join(homedir(), '.cognitive', 'modules');
14
+ function assertSafeModuleName(name) {
15
+ const trimmed = name.trim();
16
+ if (!trimmed) {
17
+ throw new Error('Invalid module name: empty');
18
+ }
19
+ if (trimmed.includes('..') || trimmed.includes('/') || trimmed.includes('\\')) {
20
+ throw new Error(`Invalid module name: ${name}`);
21
+ }
22
+ if (isAbsolute(trimmed)) {
23
+ throw new Error(`Invalid module name (absolute path not allowed): ${name}`);
24
+ }
25
+ return trimmed;
26
+ }
27
+ function resolveModuleTarget(moduleName) {
28
+ const safeName = assertSafeModuleName(moduleName);
29
+ const targetPath = resolve(USER_MODULES_DIR, safeName);
30
+ const root = resolve(USER_MODULES_DIR) + sep;
31
+ if (!targetPath.startsWith(root)) {
32
+ throw new Error(`Invalid module name (path traversal): ${moduleName}`);
33
+ }
34
+ return targetPath;
35
+ }
13
36
  /**
14
37
  * Get module version from installed module
15
38
  */
16
39
  async function getInstalledVersion(moduleName) {
17
- const modulePath = join(USER_MODULES_DIR, moduleName);
40
+ const modulePath = resolveModuleTarget(moduleName);
18
41
  if (!existsSync(modulePath)) {
19
42
  return undefined;
20
43
  }
@@ -46,29 +69,97 @@ async function getInstalledVersion(moduleName) {
46
69
  * Update an installed module
47
70
  */
48
71
  export async function update(moduleName, ctx, options = {}) {
72
+ let safeName;
73
+ try {
74
+ safeName = assertSafeModuleName(moduleName);
75
+ }
76
+ catch (e) {
77
+ return {
78
+ success: false,
79
+ error: e instanceof Error ? e.message : String(e),
80
+ };
81
+ }
49
82
  // Get installation info
50
- const info = await getInstallInfo(moduleName);
83
+ const info = await getInstallInfo(safeName);
51
84
  if (!info) {
52
85
  return {
53
86
  success: false,
54
- error: `Module not found or not installed from GitHub: ${moduleName}. Only modules installed with 'cog add' can be updated.`,
87
+ error: `Module not found or not installed with 'cog add': ${safeName}. Only modules installed with 'cog add' can be updated.`,
55
88
  };
56
89
  }
90
+ // Get current version
91
+ const oldVersion = await getInstalledVersion(safeName);
92
+ // Check if module was installed from registry
93
+ if (info.registryModule) {
94
+ const registryModule = info.registryModule;
95
+ // Use undefined instead of empty string for default registry URL
96
+ const registryUrl = info.registryUrl || undefined;
97
+ // Check registry for latest version
98
+ const client = new RegistryClient(registryUrl);
99
+ const registryInfo = await client.getModule(registryModule);
100
+ if (!registryInfo) {
101
+ return {
102
+ success: false,
103
+ error: `Module '${registryModule}' is no longer available in the registry.`,
104
+ };
105
+ }
106
+ // Warn if deprecated
107
+ if (registryInfo.deprecated) {
108
+ console.warn(`Warning: Module '${registryModule}' is deprecated.`);
109
+ }
110
+ const targetVersion = options.tag || registryInfo.version;
111
+ // Re-install from registry with optional version
112
+ const moduleSpec = options.tag ? `${registryModule}@${options.tag}` : registryModule;
113
+ const result = await addFromRegistry(moduleSpec, ctx, {
114
+ name: safeName,
115
+ registry: registryUrl,
116
+ });
117
+ if (!result.success) {
118
+ return result;
119
+ }
120
+ const data = result.data;
121
+ const newVersion = data.version || targetVersion;
122
+ // Determine message
123
+ let message;
124
+ if (oldVersion && newVersion) {
125
+ if (oldVersion === newVersion) {
126
+ message = `Already up to date: ${safeName} v${newVersion}`;
127
+ }
128
+ else {
129
+ message = `Updated: ${safeName} v${oldVersion} → v${newVersion}`;
130
+ }
131
+ }
132
+ else if (newVersion) {
133
+ message = `Updated: ${safeName} to v${newVersion}`;
134
+ }
135
+ else {
136
+ message = `Updated: ${safeName}`;
137
+ }
138
+ return {
139
+ success: true,
140
+ data: {
141
+ message,
142
+ name: safeName,
143
+ oldVersion,
144
+ newVersion,
145
+ source: 'registry',
146
+ },
147
+ };
148
+ }
149
+ // GitHub-based update
57
150
  if (!info.githubUrl) {
58
151
  return {
59
152
  success: false,
60
- error: `Module was not installed from GitHub: ${moduleName}`,
153
+ error: `Module was not installed from GitHub or registry: ${moduleName}`,
61
154
  };
62
155
  }
63
- // Get current version
64
- const oldVersion = await getInstalledVersion(moduleName);
65
156
  // Determine what ref to use
66
157
  const tag = options.tag || info.tag;
67
158
  const branch = info.branch || 'main';
68
159
  // Re-install from source
69
160
  const result = await add(info.githubUrl, ctx, {
70
161
  module: info.modulePath,
71
- name: moduleName,
162
+ name: safeName,
72
163
  tag,
73
164
  branch: tag ? undefined : branch,
74
165
  });
@@ -81,25 +172,26 @@ export async function update(moduleName, ctx, options = {}) {
81
172
  let message;
82
173
  if (oldVersion && newVersion) {
83
174
  if (oldVersion === newVersion) {
84
- message = `Already up to date: ${moduleName} v${newVersion}`;
175
+ message = `Already up to date: ${safeName} v${newVersion}`;
85
176
  }
86
177
  else {
87
- message = `Updated: ${moduleName} v${oldVersion} → v${newVersion}`;
178
+ message = `Updated: ${safeName} v${oldVersion} → v${newVersion}`;
88
179
  }
89
180
  }
90
181
  else if (newVersion) {
91
- message = `Updated: ${moduleName} to v${newVersion}`;
182
+ message = `Updated: ${safeName} to v${newVersion}`;
92
183
  }
93
184
  else {
94
- message = `Updated: ${moduleName}`;
185
+ message = `Updated: ${safeName}`;
95
186
  }
96
187
  return {
97
188
  success: true,
98
189
  data: {
99
190
  message,
100
- name: moduleName,
191
+ name: safeName,
101
192
  oldVersion,
102
193
  newVersion,
194
+ source: 'github',
103
195
  },
104
196
  };
105
197
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * cog validate - Validate a Cognitive Module's structure and examples
3
+ *
4
+ * Aligns with Python CLI's `cog validate` command.
5
+ */
6
+ import type { CommandContext, CommandResult } from '../types.js';
7
+ export interface ValidateOptions {
8
+ /** Enable strict v2.2 validation */
9
+ v22?: boolean;
10
+ /** Output format: 'text' or 'json' */
11
+ format?: 'text' | 'json';
12
+ }
13
+ export interface ValidateResult {
14
+ valid: boolean;
15
+ modulePath: string;
16
+ moduleName?: string;
17
+ errors: string[];
18
+ warnings: string[];
19
+ }
20
+ /**
21
+ * Validate a cognitive module's structure and examples.
22
+ *
23
+ * @param nameOrPath Module name or path
24
+ * @param ctx Command context
25
+ * @param options Validation options
26
+ * @returns Validation result
27
+ */
28
+ export declare function validate(nameOrPath: string, ctx: CommandContext, options?: ValidateOptions): Promise<CommandResult>;
29
+ /**
30
+ * Validate all modules in the search paths.
31
+ *
32
+ * @param ctx Command context
33
+ * @param options Validation options
34
+ * @returns Batch validation results
35
+ */
36
+ export declare function validateAll(ctx: CommandContext, options?: ValidateOptions): Promise<CommandResult>;