cognitive-modules-cli 2.2.0 → 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 +572 -28
- package/dist/commands/add.d.ts +33 -14
- package/dist/commands/add.js +222 -13
- package/dist/commands/compose.d.ts +31 -0
- package/dist/commands/compose.js +185 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.js +5 -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/index.d.ts +2 -2
- package/dist/index.js +5 -1
- package/dist/mcp/server.js +84 -79
- package/dist/modules/composition.d.ts +251 -0
- package/dist/modules/composition.js +1330 -0
- package/dist/modules/index.d.ts +2 -0
- package/dist/modules/index.js +2 -0
- package/dist/modules/loader.d.ts +22 -2
- package/dist/modules/loader.js +171 -6
- package/dist/modules/runner.d.ts +422 -1
- package/dist/modules/runner.js +1472 -71
- package/dist/modules/subagent.d.ts +6 -1
- package/dist/modules/subagent.js +20 -13
- package/dist/modules/validator.d.ts +28 -0
- package/dist/modules/validator.js +637 -0
- 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 +123 -8
- package/dist/types.js +4 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +32 -7
- package/src/cli.ts +0 -410
- package/src/commands/add.ts +0 -315
- package/src/commands/index.ts +0 -12
- 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 -55
- package/src/mcp/index.ts +0 -5
- package/src/mcp/server.ts +0 -403
- package/src/modules/index.ts +0 -7
- package/src/modules/loader.ts +0 -318
- package/src/modules/runner.ts +0 -495
- package/src/modules/subagent.ts +0 -275
- 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 -495
- package/tsconfig.json +0 -17
package/dist/providers/openai.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenAI Provider - OpenAI API (and compatible APIs)
|
|
3
|
+
*
|
|
4
|
+
* Supports both streaming and non-streaming invocation.
|
|
3
5
|
*/
|
|
4
6
|
import { BaseProvider } from './base.js';
|
|
5
7
|
export class OpenAIProvider extends BaseProvider {
|
|
@@ -16,6 +18,12 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
16
18
|
isConfigured() {
|
|
17
19
|
return !!this.apiKey;
|
|
18
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* OpenAI supports streaming.
|
|
23
|
+
*/
|
|
24
|
+
supportsStreaming() {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
19
27
|
async invoke(params) {
|
|
20
28
|
if (!this.isConfigured()) {
|
|
21
29
|
throw new Error('OpenAI API key not configured. Set OPENAI_API_KEY environment variable.');
|
|
@@ -64,4 +72,129 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
64
72
|
} : undefined,
|
|
65
73
|
};
|
|
66
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Stream-based invoke using OpenAI's streaming API.
|
|
77
|
+
* Yields content chunks as they arrive from the API.
|
|
78
|
+
*/
|
|
79
|
+
async *invokeStream(params) {
|
|
80
|
+
if (!this.isConfigured()) {
|
|
81
|
+
throw new Error('OpenAI API key not configured. Set OPENAI_API_KEY environment variable.');
|
|
82
|
+
}
|
|
83
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
84
|
+
const body = {
|
|
85
|
+
model: this.model,
|
|
86
|
+
messages: params.messages,
|
|
87
|
+
temperature: params.temperature ?? 0.7,
|
|
88
|
+
max_tokens: params.maxTokens ?? 4096,
|
|
89
|
+
stream: true,
|
|
90
|
+
stream_options: { include_usage: true },
|
|
91
|
+
};
|
|
92
|
+
// Add JSON mode if schema provided
|
|
93
|
+
if (params.jsonSchema) {
|
|
94
|
+
body.response_format = { type: 'json_object' };
|
|
95
|
+
// Append schema instruction to last user message
|
|
96
|
+
const lastUserIdx = params.messages.findLastIndex(m => m.role === 'user');
|
|
97
|
+
if (lastUserIdx >= 0) {
|
|
98
|
+
const messages = [...params.messages];
|
|
99
|
+
messages[lastUserIdx] = {
|
|
100
|
+
...messages[lastUserIdx],
|
|
101
|
+
content: messages[lastUserIdx].content + this.buildJsonPrompt(params.jsonSchema),
|
|
102
|
+
};
|
|
103
|
+
body.messages = messages;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const response = await fetch(url, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: {
|
|
109
|
+
'Content-Type': 'application/json',
|
|
110
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify(body),
|
|
113
|
+
});
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const error = await response.text();
|
|
116
|
+
throw new Error(`OpenAI API error: ${response.status} - ${error}`);
|
|
117
|
+
}
|
|
118
|
+
if (!response.body) {
|
|
119
|
+
throw new Error('OpenAI API returned no body for streaming request');
|
|
120
|
+
}
|
|
121
|
+
const reader = response.body.getReader();
|
|
122
|
+
const decoder = new TextDecoder('utf-8');
|
|
123
|
+
const collectedChunks = [];
|
|
124
|
+
let usage;
|
|
125
|
+
let buffer = '';
|
|
126
|
+
try {
|
|
127
|
+
while (true) {
|
|
128
|
+
const { done, value } = await reader.read();
|
|
129
|
+
if (done)
|
|
130
|
+
break;
|
|
131
|
+
buffer += decoder.decode(value, { stream: true });
|
|
132
|
+
// Process complete lines from the buffer
|
|
133
|
+
const lines = buffer.split('\n');
|
|
134
|
+
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
const trimmed = line.trim();
|
|
137
|
+
if (!trimmed || trimmed === 'data: [DONE]')
|
|
138
|
+
continue;
|
|
139
|
+
if (trimmed.startsWith('data: ')) {
|
|
140
|
+
try {
|
|
141
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
142
|
+
// Extract content chunk
|
|
143
|
+
const content = data.choices?.[0]?.delta?.content;
|
|
144
|
+
if (content) {
|
|
145
|
+
collectedChunks.push(content);
|
|
146
|
+
yield content;
|
|
147
|
+
}
|
|
148
|
+
// Extract usage info (usually in the last chunk)
|
|
149
|
+
if (data.usage) {
|
|
150
|
+
usage = {
|
|
151
|
+
promptTokens: data.usage.prompt_tokens || 0,
|
|
152
|
+
completionTokens: data.usage.completion_tokens || 0,
|
|
153
|
+
totalTokens: data.usage.total_tokens || 0,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Skip invalid JSON chunks
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Flush decoder and process trailing buffered data even without trailing newline.
|
|
164
|
+
buffer += decoder.decode();
|
|
165
|
+
for (const line of buffer.split('\n')) {
|
|
166
|
+
const trimmed = line.trim();
|
|
167
|
+
if (!trimmed || trimmed === 'data: [DONE]')
|
|
168
|
+
continue;
|
|
169
|
+
if (trimmed.startsWith('data: ')) {
|
|
170
|
+
try {
|
|
171
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
172
|
+
const content = data.choices?.[0]?.delta?.content;
|
|
173
|
+
if (content) {
|
|
174
|
+
collectedChunks.push(content);
|
|
175
|
+
yield content;
|
|
176
|
+
}
|
|
177
|
+
if (data.usage) {
|
|
178
|
+
usage = {
|
|
179
|
+
promptTokens: data.usage.prompt_tokens || 0,
|
|
180
|
+
completionTokens: data.usage.completion_tokens || 0,
|
|
181
|
+
totalTokens: data.usage.total_tokens || 0,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Skip invalid JSON chunks
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
reader.releaseLock();
|
|
193
|
+
}
|
|
194
|
+
const fullContent = collectedChunks.join('');
|
|
195
|
+
return {
|
|
196
|
+
content: fullContent,
|
|
197
|
+
usage,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
67
200
|
}
|
|
@@ -0,0 +1,204 @@
|
|
|
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
|
+
/** v1 Registry Format (current cognitive-registry.json) */
|
|
12
|
+
export interface RegistryV1 {
|
|
13
|
+
$schema?: string;
|
|
14
|
+
version: string;
|
|
15
|
+
updated: string;
|
|
16
|
+
modules: {
|
|
17
|
+
[name: string]: {
|
|
18
|
+
description: string;
|
|
19
|
+
version: string;
|
|
20
|
+
source: string;
|
|
21
|
+
tags: string[];
|
|
22
|
+
author: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
categories?: {
|
|
26
|
+
[key: string]: {
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
modules: string[];
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** v2 Registry Format (new format per REGISTRY-PROTOCOL.md) */
|
|
34
|
+
export interface RegistryV2 {
|
|
35
|
+
$schema?: string;
|
|
36
|
+
version: string;
|
|
37
|
+
updated: string;
|
|
38
|
+
modules: {
|
|
39
|
+
[name: string]: RegistryEntryV2;
|
|
40
|
+
};
|
|
41
|
+
categories?: {
|
|
42
|
+
[key: string]: {
|
|
43
|
+
name: string;
|
|
44
|
+
name_zh?: string;
|
|
45
|
+
description: string;
|
|
46
|
+
modules: string[];
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
featured?: string[];
|
|
50
|
+
stats?: {
|
|
51
|
+
total_modules: number;
|
|
52
|
+
total_downloads: number;
|
|
53
|
+
last_updated: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** v2 Registry Entry */
|
|
57
|
+
export interface RegistryEntryV2 {
|
|
58
|
+
identity: {
|
|
59
|
+
name: string;
|
|
60
|
+
namespace: string;
|
|
61
|
+
version: string;
|
|
62
|
+
spec_version: string;
|
|
63
|
+
};
|
|
64
|
+
metadata: {
|
|
65
|
+
description: string;
|
|
66
|
+
description_zh?: string;
|
|
67
|
+
author: string;
|
|
68
|
+
license?: string;
|
|
69
|
+
repository?: string;
|
|
70
|
+
documentation?: string;
|
|
71
|
+
homepage?: string;
|
|
72
|
+
keywords?: string[];
|
|
73
|
+
tier?: 'exec' | 'decision' | 'exploration';
|
|
74
|
+
};
|
|
75
|
+
quality?: {
|
|
76
|
+
conformance_level?: number;
|
|
77
|
+
test_coverage?: number;
|
|
78
|
+
test_vector_pass?: boolean;
|
|
79
|
+
verified?: boolean;
|
|
80
|
+
verified_by?: string;
|
|
81
|
+
verified_at?: string;
|
|
82
|
+
downloads_30d?: number;
|
|
83
|
+
stars?: number;
|
|
84
|
+
badges?: string[];
|
|
85
|
+
deprecated?: boolean;
|
|
86
|
+
successor?: string;
|
|
87
|
+
deprecation_reason?: string;
|
|
88
|
+
};
|
|
89
|
+
dependencies: {
|
|
90
|
+
runtime_min: string;
|
|
91
|
+
modules: string[];
|
|
92
|
+
};
|
|
93
|
+
distribution: {
|
|
94
|
+
tarball?: string;
|
|
95
|
+
checksum?: string;
|
|
96
|
+
size_bytes?: number;
|
|
97
|
+
files?: string[];
|
|
98
|
+
source?: string;
|
|
99
|
+
};
|
|
100
|
+
timestamps?: {
|
|
101
|
+
created_at?: string;
|
|
102
|
+
updated_at?: string;
|
|
103
|
+
deprecated_at?: string;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Normalized module info (works with both v1 and v2) */
|
|
107
|
+
export interface ModuleInfo {
|
|
108
|
+
name: string;
|
|
109
|
+
version: string;
|
|
110
|
+
description: string;
|
|
111
|
+
author: string;
|
|
112
|
+
source: string;
|
|
113
|
+
keywords: string[];
|
|
114
|
+
tier?: string;
|
|
115
|
+
namespace?: string;
|
|
116
|
+
license?: string;
|
|
117
|
+
repository?: string;
|
|
118
|
+
conformance_level?: number;
|
|
119
|
+
verified?: boolean;
|
|
120
|
+
deprecated?: boolean;
|
|
121
|
+
}
|
|
122
|
+
/** Search result */
|
|
123
|
+
export interface SearchResult {
|
|
124
|
+
name: string;
|
|
125
|
+
description: string;
|
|
126
|
+
version: string;
|
|
127
|
+
score: number;
|
|
128
|
+
keywords: string[];
|
|
129
|
+
}
|
|
130
|
+
export declare class RegistryClient {
|
|
131
|
+
private registryUrl;
|
|
132
|
+
private cache;
|
|
133
|
+
constructor(registryUrl?: string);
|
|
134
|
+
private parseRegistryResponse;
|
|
135
|
+
/**
|
|
136
|
+
* Generate a unique cache filename based on registry URL
|
|
137
|
+
*/
|
|
138
|
+
private getCacheFileName;
|
|
139
|
+
/**
|
|
140
|
+
* Fetch registry index (with caching)
|
|
141
|
+
*/
|
|
142
|
+
fetchRegistry(forceRefresh?: boolean): Promise<RegistryV1 | RegistryV2>;
|
|
143
|
+
/**
|
|
144
|
+
* Check if registry is v2 format
|
|
145
|
+
*/
|
|
146
|
+
private isV2Registry;
|
|
147
|
+
/**
|
|
148
|
+
* Normalize module entry to unified format
|
|
149
|
+
*/
|
|
150
|
+
private normalizeModule;
|
|
151
|
+
/**
|
|
152
|
+
* List all modules in registry
|
|
153
|
+
*/
|
|
154
|
+
listModules(): Promise<ModuleInfo[]>;
|
|
155
|
+
/**
|
|
156
|
+
* Get a specific module by name
|
|
157
|
+
*/
|
|
158
|
+
getModule(name: string): Promise<ModuleInfo | null>;
|
|
159
|
+
/**
|
|
160
|
+
* Search modules by query
|
|
161
|
+
*/
|
|
162
|
+
search(query: string): Promise<SearchResult[]>;
|
|
163
|
+
/**
|
|
164
|
+
* Get categories
|
|
165
|
+
*/
|
|
166
|
+
getCategories(): Promise<{
|
|
167
|
+
[key: string]: {
|
|
168
|
+
name: string;
|
|
169
|
+
description: string;
|
|
170
|
+
modules: string[];
|
|
171
|
+
};
|
|
172
|
+
}>;
|
|
173
|
+
/**
|
|
174
|
+
* Parse GitHub source string
|
|
175
|
+
* Format: github:<owner>/<repo>[/<path>][@<ref>]
|
|
176
|
+
*/
|
|
177
|
+
parseGitHubSource(source: string): {
|
|
178
|
+
org: string;
|
|
179
|
+
repo: string;
|
|
180
|
+
path?: string;
|
|
181
|
+
ref?: string;
|
|
182
|
+
} | null;
|
|
183
|
+
/**
|
|
184
|
+
* Verify checksum of downloaded file
|
|
185
|
+
*/
|
|
186
|
+
verifyChecksum(filePath: string, expected: string): Promise<boolean>;
|
|
187
|
+
/**
|
|
188
|
+
* Get the download URL for a module
|
|
189
|
+
*/
|
|
190
|
+
getDownloadUrl(moduleName: string): Promise<{
|
|
191
|
+
url: string;
|
|
192
|
+
isGitHub: boolean;
|
|
193
|
+
githubInfo?: {
|
|
194
|
+
org: string;
|
|
195
|
+
repo: string;
|
|
196
|
+
path?: string;
|
|
197
|
+
ref?: string;
|
|
198
|
+
};
|
|
199
|
+
}>;
|
|
200
|
+
}
|
|
201
|
+
export declare const defaultRegistry: RegistryClient;
|
|
202
|
+
export declare function listRegistryModules(): Promise<ModuleInfo[]>;
|
|
203
|
+
export declare function getRegistryModule(name: string): Promise<ModuleInfo | null>;
|
|
204
|
+
export declare function searchRegistry(query: string): Promise<SearchResult[]>;
|
|
@@ -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
|
+
}
|