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
@@ -1,8 +1,13 @@
1
1
  /**
2
- * Add command - Install modules from GitHub
2
+ * Add command - Install modules from GitHub or Registry
3
3
  *
4
- * cog add ziel-io/cognitive-modules -m code-simplifier
5
- * cog add https://github.com/org/repo --module name --tag v1.0.0
4
+ * From Registry:
5
+ * cog add code-reviewer
6
+ * cog add code-reviewer@2.0.0
7
+ *
8
+ * From GitHub:
9
+ * cog add ziel-io/cognitive-modules -m code-simplifier
10
+ * cog add https://github.com/org/repo --module name --tag v1.0.0
6
11
  */
7
12
  import type { CommandContext, CommandResult } from '../types.js';
8
13
  export interface AddOptions {
@@ -10,25 +15,39 @@ export interface AddOptions {
10
15
  name?: string;
11
16
  branch?: string;
12
17
  tag?: string;
18
+ registry?: string;
19
+ }
20
+ export interface InstallInfo {
21
+ source: string;
22
+ githubUrl: string;
23
+ modulePath?: string;
24
+ tag?: string;
25
+ branch?: string;
26
+ version?: string;
27
+ installedAt: string;
28
+ installedTime: string;
29
+ /** Registry module name if installed from registry */
30
+ registryModule?: string;
31
+ /** Registry URL if installed from a custom registry */
32
+ registryUrl?: string;
13
33
  }
14
34
  interface InstallManifest {
15
- [moduleName: string]: {
16
- source: string;
17
- githubUrl: string;
18
- modulePath?: string;
19
- tag?: string;
20
- branch?: string;
21
- version?: string;
22
- installedAt: string;
23
- installedTime: string;
24
- };
35
+ [moduleName: string]: InstallInfo;
25
36
  }
26
37
  /**
27
38
  * Get installation info for a module
28
39
  */
29
40
  export declare function getInstallInfo(moduleName: string): Promise<InstallManifest[string] | null>;
41
+ /**
42
+ * Add a module from the registry
43
+ */
44
+ export declare function addFromRegistry(moduleSpec: string, ctx: CommandContext, options?: AddOptions): Promise<CommandResult>;
30
45
  /**
31
46
  * Add a module from GitHub
32
47
  */
33
- export declare function add(url: string, ctx: CommandContext, options?: AddOptions): Promise<CommandResult>;
48
+ export declare function addFromGitHub(url: string, ctx: CommandContext, options?: AddOptions): Promise<CommandResult>;
49
+ /**
50
+ * Add a module from GitHub or Registry (auto-detect source)
51
+ */
52
+ export declare function add(source: string, ctx: CommandContext, options?: AddOptions): Promise<CommandResult>;
34
53
  export {};
@@ -1,15 +1,21 @@
1
1
  /**
2
- * Add command - Install modules from GitHub
2
+ * Add command - Install modules from GitHub or Registry
3
3
  *
4
- * cog add ziel-io/cognitive-modules -m code-simplifier
5
- * cog add https://github.com/org/repo --module name --tag v1.0.0
4
+ * From Registry:
5
+ * cog add code-reviewer
6
+ * cog add code-reviewer@2.0.0
7
+ *
8
+ * From GitHub:
9
+ * cog add ziel-io/cognitive-modules -m code-simplifier
10
+ * cog add https://github.com/org/repo --module name --tag v1.0.0
6
11
  */
7
12
  import { createWriteStream, existsSync, mkdirSync, rmSync, readdirSync, statSync, copyFileSync } from 'node:fs';
8
13
  import { writeFile, readFile, mkdir } from 'node:fs/promises';
9
14
  import { pipeline } from 'node:stream/promises';
10
15
  import { Readable } from 'node:stream';
11
- import { join, basename } from 'node:path';
16
+ import { join, basename, dirname, resolve, sep, isAbsolute } from 'node:path';
12
17
  import { homedir, tmpdir } from 'node:os';
18
+ import { RegistryClient } from '../registry/client.js';
13
19
  // Module storage paths
14
20
  const USER_MODULES_DIR = join(homedir(), '.cognitive', 'modules');
15
21
  const INSTALLED_MANIFEST = join(homedir(), '.cognitive', 'installed.json');
@@ -33,6 +39,33 @@ function parseGitHubUrl(url) {
33
39
  fullUrl: `https://github.com/${org}/${repo}`,
34
40
  };
35
41
  }
42
+ function assertSafeModuleName(name) {
43
+ const trimmed = name.trim();
44
+ if (!trimmed) {
45
+ throw new Error('Invalid module name: empty');
46
+ }
47
+ if (trimmed.includes('..') || trimmed.includes('/') || trimmed.includes('\\')) {
48
+ throw new Error(`Invalid module name: ${name}`);
49
+ }
50
+ if (isAbsolute(trimmed)) {
51
+ throw new Error(`Invalid module name (absolute path not allowed): ${name}`);
52
+ }
53
+ return trimmed;
54
+ }
55
+ function resolveModuleTarget(moduleName) {
56
+ const safeName = assertSafeModuleName(moduleName);
57
+ const targetPath = resolve(USER_MODULES_DIR, safeName);
58
+ const root = resolve(USER_MODULES_DIR) + sep;
59
+ if (!targetPath.startsWith(root)) {
60
+ throw new Error(`Invalid module name (path traversal): ${moduleName}`);
61
+ }
62
+ return targetPath;
63
+ }
64
+ function isPathWithinRoot(rootDir, targetPath) {
65
+ const root = resolve(rootDir);
66
+ const target = resolve(targetPath);
67
+ return target === root || target.startsWith(root + sep);
68
+ }
36
69
  /**
37
70
  * Download and extract ZIP from GitHub
38
71
  */
@@ -56,6 +89,9 @@ async function downloadAndExtract(org, repo, ref, isTag) {
56
89
  await pipeline(Readable.fromWeb(response.body), fileStream);
57
90
  // Extract using built-in unzip (available on most systems)
58
91
  const { execSync } = await import('node:child_process');
92
+ // Validate ZIP entries to avoid path traversal (Zip Slip)
93
+ const listing = execSync(`unzip -Z1 "${zipPath}"`, { stdio: 'pipe' }).toString('utf-8');
94
+ assertSafeZipEntries(listing);
59
95
  execSync(`unzip -q "${zipPath}" -d "${tempDir}"`, { stdio: 'pipe' });
60
96
  // Find extracted directory
61
97
  const entries = readdirSync(tempDir).filter(e => e !== 'repo.zip' && statSync(join(tempDir, e)).isDirectory());
@@ -64,6 +100,22 @@ async function downloadAndExtract(org, repo, ref, isTag) {
64
100
  }
65
101
  return join(tempDir, entries[0]);
66
102
  }
103
+ /**
104
+ * Validate ZIP listing to prevent path traversal.
105
+ */
106
+ function assertSafeZipEntries(listing) {
107
+ const entries = listing.split(/\r?\n/).map(e => e.trim()).filter(Boolean);
108
+ for (const entry of entries) {
109
+ const normalized = entry.replace(/\\/g, '/');
110
+ if (normalized.startsWith('/') || /^[a-zA-Z]:\//.test(normalized)) {
111
+ throw new Error(`Unsafe ZIP entry (absolute path): ${entry}`);
112
+ }
113
+ const parts = normalized.split('/');
114
+ if (parts.includes('..')) {
115
+ throw new Error(`Unsafe ZIP entry (path traversal): ${entry}`);
116
+ }
117
+ }
118
+ }
67
119
  /**
68
120
  * Check if a directory is a valid module
69
121
  */
@@ -76,11 +128,15 @@ function isValidModule(path) {
76
128
  * Find module within repository
77
129
  */
78
130
  function findModuleInRepo(repoRoot, modulePath) {
79
- const possiblePaths = [
80
- join(repoRoot, modulePath),
81
- join(repoRoot, 'cognitive', 'modules', modulePath),
82
- join(repoRoot, 'modules', modulePath),
131
+ const candidatePaths = [
132
+ resolve(repoRoot, modulePath),
133
+ resolve(repoRoot, 'cognitive', 'modules', modulePath),
134
+ resolve(repoRoot, 'modules', modulePath),
83
135
  ];
136
+ const possiblePaths = candidatePaths.filter((p) => isPathWithinRoot(repoRoot, p));
137
+ if (possiblePaths.length === 0) {
138
+ throw new Error(`Invalid module path (outside repository root): ${modulePath}`);
139
+ }
84
140
  for (const p of possiblePaths) {
85
141
  if (existsSync(p) && isValidModule(p)) {
86
142
  return p;
@@ -157,10 +213,135 @@ export async function getInstallInfo(moduleName) {
157
213
  const manifest = JSON.parse(content);
158
214
  return manifest[moduleName] || null;
159
215
  }
216
+ /**
217
+ * Check if input looks like a GitHub URL or org/repo format
218
+ *
219
+ * GitHub formats:
220
+ * - https://github.com/org/repo
221
+ * - http://github.com/org/repo
222
+ * - github:org/repo
223
+ * - org/repo (shorthand, must be exactly two parts)
224
+ *
225
+ * NOT GitHub (registry module names):
226
+ * - code-reviewer
227
+ * - code-reviewer@1.0.0
228
+ * - my-module (no slash)
229
+ */
230
+ function isGitHubSource(input) {
231
+ // URL formats
232
+ if (input.startsWith('http://') || input.startsWith('https://')) {
233
+ return true;
234
+ }
235
+ // github: prefix format
236
+ if (input.startsWith('github:')) {
237
+ return true;
238
+ }
239
+ // Contains github.com
240
+ if (input.includes('github.com')) {
241
+ return true;
242
+ }
243
+ // Shorthand format: org/repo (exactly two parts, no @ version suffix)
244
+ // Must match pattern like: owner/repo or owner/repo but NOT module@version
245
+ const shorthandMatch = input.match(/^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/);
246
+ if (shorthandMatch) {
247
+ // Additional check: if it contains @, it's likely a scoped npm package or versioned module
248
+ // But GitHub shorthand shouldn't have @ in the middle
249
+ return !input.includes('@');
250
+ }
251
+ return false;
252
+ }
253
+ /**
254
+ * Parse module name with optional version: "module-name@1.0.0"
255
+ */
256
+ function parseModuleSpec(spec) {
257
+ const match = spec.match(/^([^@]+)(?:@(.+))?$/);
258
+ if (!match) {
259
+ return { name: spec };
260
+ }
261
+ return { name: match[1], version: match[2] };
262
+ }
263
+ /**
264
+ * Add a module from the registry
265
+ */
266
+ export async function addFromRegistry(moduleSpec, ctx, options = {}) {
267
+ const { name: moduleName, version: requestedVersion } = parseModuleSpec(moduleSpec);
268
+ const { name: customName, registry: registryUrl } = options;
269
+ try {
270
+ const client = new RegistryClient(registryUrl);
271
+ const moduleInfo = await client.getModule(moduleName);
272
+ if (!moduleInfo) {
273
+ return {
274
+ success: false,
275
+ error: `Module not found in registry: ${moduleName}\nUse 'cog search' to find available modules.`,
276
+ };
277
+ }
278
+ // Check if deprecated
279
+ if (moduleInfo.deprecated) {
280
+ console.error(`Warning: Module '${moduleName}' is deprecated.`);
281
+ }
282
+ // Get download info
283
+ const downloadInfo = await client.getDownloadUrl(moduleName);
284
+ if (downloadInfo.isGitHub && downloadInfo.githubInfo) {
285
+ const { org, repo, path, ref } = downloadInfo.githubInfo;
286
+ // Use addFromGitHub() directly (not add() to avoid recursion)
287
+ const result = await addFromGitHub(`${org}/${repo}`, ctx, {
288
+ module: path,
289
+ name: customName || moduleName,
290
+ tag: requestedVersion || ref,
291
+ branch: ref || 'main',
292
+ });
293
+ // If successful, update the install manifest to track registry info
294
+ if (result.success && result.data) {
295
+ const data = result.data;
296
+ const installName = data.name;
297
+ // Update manifest with registry info
298
+ const existingInfo = await getInstallInfo(installName);
299
+ if (existingInfo) {
300
+ await recordInstall(installName, {
301
+ ...existingInfo,
302
+ registryModule: moduleName,
303
+ registryUrl: registryUrl,
304
+ });
305
+ }
306
+ else {
307
+ // Fallback: create minimal install info if not found (shouldn't happen but safety first)
308
+ await recordInstall(installName, {
309
+ source: 'registry',
310
+ githubUrl: `${org}/${repo}`,
311
+ modulePath: path,
312
+ version: data.version,
313
+ installedAt: data.location || '',
314
+ installedTime: new Date().toISOString(),
315
+ registryModule: moduleName,
316
+ registryUrl: registryUrl,
317
+ });
318
+ }
319
+ // Update result to indicate registry source
320
+ result.data = {
321
+ ...result.data,
322
+ source: 'registry',
323
+ registryModule: moduleName,
324
+ };
325
+ }
326
+ return result;
327
+ }
328
+ // For tarball sources, download directly (future implementation)
329
+ return {
330
+ success: false,
331
+ error: `Tarball downloads not yet supported. Source: ${moduleInfo.source}`,
332
+ };
333
+ }
334
+ catch (error) {
335
+ return {
336
+ success: false,
337
+ error: error instanceof Error ? error.message : String(error),
338
+ };
339
+ }
340
+ }
160
341
  /**
161
342
  * Add a module from GitHub
162
343
  */
163
- export async function add(url, ctx, options = {}) {
344
+ export async function addFromGitHub(url, ctx, options = {}) {
164
345
  const { org, repo, fullUrl } = parseGitHubUrl(url);
165
346
  const { module: modulePath, name, branch = 'main', tag } = options;
166
347
  // Determine ref (tag takes priority)
@@ -184,11 +365,11 @@ export async function add(url, ctx, options = {}) {
184
365
  sourcePath = repoRoot;
185
366
  }
186
367
  // Determine module name
187
- moduleName = name || basename(sourcePath);
368
+ moduleName = name ? assertSafeModuleName(name) : basename(sourcePath);
188
369
  // Get version
189
370
  const version = await getModuleVersion(sourcePath);
190
371
  // Install to user modules dir
191
- const targetPath = join(USER_MODULES_DIR, moduleName);
372
+ const targetPath = resolveModuleTarget(moduleName);
192
373
  // Remove existing
193
374
  if (existsSync(targetPath)) {
194
375
  rmSync(targetPath, { recursive: true, force: true });
@@ -208,8 +389,10 @@ export async function add(url, ctx, options = {}) {
208
389
  installedTime: new Date().toISOString(),
209
390
  });
210
391
  // Cleanup temp directory
211
- const tempDir = repoRoot.split('/').slice(0, -1).join('/');
212
- rmSync(tempDir, { recursive: true, force: true });
392
+ const tempDir = dirname(repoRoot);
393
+ if (tempDir && tempDir !== '/' && tempDir !== '.' && tempDir !== USER_MODULES_DIR) {
394
+ rmSync(tempDir, { recursive: true, force: true });
395
+ }
213
396
  return {
214
397
  success: true,
215
398
  data: {
@@ -217,13 +400,39 @@ export async function add(url, ctx, options = {}) {
217
400
  name: moduleName,
218
401
  version,
219
402
  location: targetPath,
403
+ source: 'github',
220
404
  },
221
405
  };
222
406
  }
223
407
  catch (error) {
408
+ // Cleanup temp directory on error
409
+ if (repoRoot) {
410
+ try {
411
+ const tempDir = dirname(repoRoot);
412
+ if (tempDir && tempDir !== '/' && tempDir !== '.' && tempDir !== USER_MODULES_DIR) {
413
+ rmSync(tempDir, { recursive: true, force: true });
414
+ }
415
+ }
416
+ catch {
417
+ // Ignore cleanup errors
418
+ }
419
+ }
224
420
  return {
225
421
  success: false,
226
422
  error: error instanceof Error ? error.message : String(error),
227
423
  };
228
424
  }
229
425
  }
426
+ /**
427
+ * Add a module from GitHub or Registry (auto-detect source)
428
+ */
429
+ export async function add(source, ctx, options = {}) {
430
+ // Determine source type
431
+ if (isGitHubSource(source)) {
432
+ return addFromGitHub(source, ctx, options);
433
+ }
434
+ else {
435
+ // Treat as registry module name
436
+ return addFromRegistry(source, ctx, options);
437
+ }
438
+ }
@@ -8,14 +8,30 @@
8
8
  * - Iterative: A → (check) → A → ... → Done
9
9
  */
10
10
  import { findModule, getDefaultSearchPaths, executeComposition } from '../modules/index.js';
11
+ import { ErrorCodes, attachContext, makeErrorEnvelope } from '../errors/index.js';
12
+ function looksLikeCode(str) {
13
+ const codeIndicators = [
14
+ /^(def|function|class|const|let|var|import|export|public|private)\s/,
15
+ /[{};()]/,
16
+ /=>/,
17
+ /\.(py|js|ts|go|rs|java|cpp|c|rb)$/,
18
+ ];
19
+ return codeIndicators.some(re => re.test(str));
20
+ }
11
21
  export async function compose(moduleName, ctx, options = {}) {
12
22
  const searchPaths = getDefaultSearchPaths(ctx.cwd);
13
23
  // Find module
14
24
  const module = await findModule(moduleName, searchPaths);
15
25
  if (!module) {
26
+ const errorEnvelope = attachContext(makeErrorEnvelope({
27
+ code: ErrorCodes.MODULE_NOT_FOUND,
28
+ message: `Module not found: ${moduleName}\nSearch paths: ${searchPaths.join(', ')}`,
29
+ suggestion: "Use 'cog list' to see installed modules or 'cog search' to find modules in registry",
30
+ }), { module: moduleName, provider: ctx.provider.name });
16
31
  return {
17
32
  success: false,
18
- error: `Module not found: ${moduleName}\nSearch paths: ${searchPaths.join(', ')}`,
33
+ error: errorEnvelope.error.message,
34
+ data: errorEnvelope,
19
35
  };
20
36
  }
21
37
  try {
@@ -26,16 +42,26 @@ export async function compose(moduleName, ctx, options = {}) {
26
42
  inputData = JSON.parse(options.input);
27
43
  }
28
44
  catch {
45
+ const errorEnvelope = attachContext(makeErrorEnvelope({
46
+ code: ErrorCodes.INVALID_INPUT,
47
+ message: `Invalid JSON input: ${options.input}`,
48
+ suggestion: 'Ensure input is valid JSON format',
49
+ }), { module: moduleName, provider: ctx.provider.name });
29
50
  return {
30
51
  success: false,
31
- error: `Invalid JSON input: ${options.input}`,
52
+ error: errorEnvelope.error.message,
53
+ data: errorEnvelope,
32
54
  };
33
55
  }
34
56
  }
35
57
  // Handle --args as text input
36
58
  if (options.args) {
37
- inputData.query = options.args;
38
- inputData.code = options.args;
59
+ if (looksLikeCode(options.args)) {
60
+ inputData.code = options.args;
61
+ }
62
+ else {
63
+ inputData.query = options.args;
64
+ }
39
65
  }
40
66
  // Execute composition
41
67
  const result = await executeComposition(moduleName, inputData, ctx.provider, {
@@ -56,11 +82,27 @@ export async function compose(moduleName, ctx, options = {}) {
56
82
  }
57
83
  console.error(`--- Total: ${result.totalTimeMs}ms ---`);
58
84
  }
85
+ if (!result.ok) {
86
+ const partialData = options.trace
87
+ ? { moduleResults: result.moduleResults, trace: result.trace }
88
+ : { moduleResults: result.moduleResults };
89
+ const errorEnvelope = attachContext(makeErrorEnvelope({
90
+ code: result.error?.code ?? ErrorCodes.INTERNAL_ERROR,
91
+ message: result.error?.message ?? 'Composition failed',
92
+ details: result.error?.module ? { module: result.error.module } : undefined,
93
+ partial_data: partialData,
94
+ }), { module: moduleName, provider: ctx.provider.name });
95
+ return {
96
+ success: false,
97
+ error: errorEnvelope.error.message,
98
+ data: errorEnvelope,
99
+ };
100
+ }
59
101
  // Return result
60
102
  if (options.trace) {
61
103
  // Include full result with trace
62
104
  return {
63
- success: result.ok,
105
+ success: true,
64
106
  data: {
65
107
  ok: result.ok,
66
108
  result: result.result,
@@ -73,33 +115,28 @@ export async function compose(moduleName, ctx, options = {}) {
73
115
  }
74
116
  else if (options.pretty) {
75
117
  return {
76
- success: result.ok,
118
+ success: true,
77
119
  data: result.result,
78
120
  };
79
121
  }
80
122
  else {
81
- // For non-pretty mode, return data (success) or error (failure)
82
- if (result.ok && result.result) {
83
- return {
84
- success: true,
85
- data: result.result.data,
86
- };
87
- }
88
- else {
89
- return {
90
- success: false,
91
- error: result.error
92
- ? `${result.error.code}: ${result.error.message}`
93
- : 'Composition failed',
94
- data: result.moduleResults,
95
- };
96
- }
123
+ // Keep compose output consistent with run/pipe: always return full v2.2 envelope
124
+ return {
125
+ success: true,
126
+ data: result.result,
127
+ };
97
128
  }
98
129
  }
99
130
  catch (e) {
131
+ const errorEnvelope = attachContext(makeErrorEnvelope({
132
+ code: ErrorCodes.INTERNAL_ERROR,
133
+ message: e instanceof Error ? e.message : String(e),
134
+ recoverable: false,
135
+ }), { module: moduleName, provider: ctx.provider.name });
100
136
  return {
101
137
  success: false,
102
- error: e instanceof Error ? e.message : String(e),
138
+ error: errorEnvelope.error.message,
139
+ data: errorEnvelope,
103
140
  };
104
141
  }
105
142
  }
@@ -10,3 +10,7 @@ export * from './update.js';
10
10
  export * from './remove.js';
11
11
  export * from './versions.js';
12
12
  export * from './compose.js';
13
+ export * from './validate.js';
14
+ export * from './migrate.js';
15
+ export * from './test.js';
16
+ export * from './search.js';
@@ -10,3 +10,7 @@ export * from './update.js';
10
10
  export * from './remove.js';
11
11
  export * from './versions.js';
12
12
  export * from './compose.js';
13
+ export * from './validate.js';
14
+ export * from './migrate.js';
15
+ export * from './test.js';
16
+ export * from './search.js';
@@ -3,6 +3,28 @@
3
3
  */
4
4
  import * as fs from 'node:fs/promises';
5
5
  import * as path from 'node:path';
6
+ function assertSafeModuleName(name) {
7
+ const trimmed = name.trim();
8
+ if (!trimmed) {
9
+ throw new Error('Invalid module name: empty');
10
+ }
11
+ if (trimmed.includes('..') || trimmed.includes('/') || trimmed.includes('\\')) {
12
+ throw new Error(`Invalid module name: ${name}`);
13
+ }
14
+ if (path.isAbsolute(trimmed)) {
15
+ throw new Error(`Invalid module name (absolute path not allowed): ${name}`);
16
+ }
17
+ return trimmed;
18
+ }
19
+ function resolveModuleDir(rootDir, moduleName) {
20
+ const safeName = assertSafeModuleName(moduleName);
21
+ const targetPath = path.resolve(rootDir, safeName);
22
+ const root = path.resolve(rootDir) + path.sep;
23
+ if (!targetPath.startsWith(root)) {
24
+ throw new Error(`Invalid module name (path traversal): ${moduleName}`);
25
+ }
26
+ return targetPath;
27
+ }
6
28
  const EXAMPLE_MODULE_MD = `---
7
29
  name: my-module
8
30
  version: 1.0.0
@@ -46,7 +68,7 @@ export async function init(ctx, moduleName) {
46
68
  await fs.mkdir(cognitiveDir, { recursive: true });
47
69
  // If module name provided, create a new module
48
70
  if (moduleName) {
49
- const moduleDir = path.join(cognitiveDir, moduleName);
71
+ const moduleDir = resolveModuleDir(cognitiveDir, moduleName);
50
72
  await fs.mkdir(moduleDir, { recursive: true });
51
73
  await fs.writeFile(path.join(moduleDir, 'MODULE.md'), EXAMPLE_MODULE_MD.replace('my-module', moduleName));
52
74
  await fs.writeFile(path.join(moduleDir, 'schema.json'), EXAMPLE_SCHEMA_JSON);
@@ -0,0 +1,30 @@
1
+ /**
2
+ * cog migrate - Migrate modules to v2.2 format
3
+ *
4
+ * Aligns with Python CLI's `cog migrate` command.
5
+ * Performs migration from v0/v1/v2.1 to v2.2 format.
6
+ */
7
+ import type { CommandContext, CommandResult } from '../types.js';
8
+ export interface MigrateOptions {
9
+ /** Dry run - only show what would be done */
10
+ dryRun?: boolean;
11
+ /** Create backup before migration */
12
+ backup?: boolean;
13
+ /** Migrate all modules */
14
+ all?: boolean;
15
+ }
16
+ export interface MigrateResult {
17
+ moduleName: string;
18
+ modulePath: string;
19
+ success: boolean;
20
+ changes: string[];
21
+ warnings: string[];
22
+ }
23
+ /**
24
+ * Migrate a single module to v2.2 format.
25
+ */
26
+ export declare function migrate(nameOrPath: string, ctx: CommandContext, options?: MigrateOptions): Promise<CommandResult>;
27
+ /**
28
+ * Migrate all modules to v2.2 format.
29
+ */
30
+ export declare function migrateAll(ctx: CommandContext, options?: MigrateOptions): Promise<CommandResult>;