cognitive-modules 0.3.0__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- cognitive/__init__.py +1 -1
- cognitive/cli.py +371 -14
- cognitive/loader.py +180 -14
- cognitive/mcp_server.py +245 -0
- cognitive/migrate.py +624 -0
- cognitive/registry.py +325 -11
- cognitive/runner.py +409 -80
- cognitive/server.py +294 -0
- cognitive/validator.py +380 -122
- cognitive_modules-0.5.0.dist-info/METADATA +431 -0
- cognitive_modules-0.5.0.dist-info/RECORD +18 -0
- cognitive_modules-0.5.0.dist-info/entry_points.txt +2 -0
- cognitive_modules-0.3.0.dist-info/METADATA +0 -418
- cognitive_modules-0.3.0.dist-info/RECORD +0 -15
- cognitive_modules-0.3.0.dist-info/entry_points.txt +0 -2
- {cognitive_modules-0.3.0.dist-info → cognitive_modules-0.5.0.dist-info}/WHEEL +0 -0
- {cognitive_modules-0.3.0.dist-info → cognitive_modules-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {cognitive_modules-0.3.0.dist-info → cognitive_modules-0.5.0.dist-info}/top_level.txt +0 -0
cognitive/registry.py
CHANGED
|
@@ -17,8 +17,11 @@ import subprocess
|
|
|
17
17
|
import tempfile
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import Optional
|
|
20
|
-
from urllib.request import urlopen
|
|
20
|
+
from urllib.request import urlopen, Request
|
|
21
21
|
from urllib.error import URLError
|
|
22
|
+
import zipfile
|
|
23
|
+
import io
|
|
24
|
+
import re
|
|
22
25
|
|
|
23
26
|
# Standard module search paths
|
|
24
27
|
SEARCH_PATHS = [
|
|
@@ -54,9 +57,11 @@ def find_module(name: str) -> Optional[Path]:
|
|
|
54
57
|
"""Find a module by name, searching all paths in order."""
|
|
55
58
|
for base_path in get_search_paths():
|
|
56
59
|
module_path = base_path / name
|
|
57
|
-
# Support
|
|
60
|
+
# Support v2, v1, and v0 formats
|
|
58
61
|
if module_path.exists():
|
|
59
|
-
if (module_path / "
|
|
62
|
+
if (module_path / "module.yaml").exists() or \
|
|
63
|
+
(module_path / "MODULE.md").exists() or \
|
|
64
|
+
(module_path / "module.md").exists():
|
|
60
65
|
return module_path
|
|
61
66
|
return None
|
|
62
67
|
|
|
@@ -75,12 +80,16 @@ def list_modules() -> list[dict]:
|
|
|
75
80
|
continue
|
|
76
81
|
if module_dir.name in seen:
|
|
77
82
|
continue
|
|
78
|
-
# Support both formats
|
|
79
|
-
if not ((module_dir / "MODULE.md").exists() or (module_dir / "module.md").exists()):
|
|
80
|
-
continue
|
|
81
83
|
|
|
82
|
-
# Detect format
|
|
83
|
-
|
|
84
|
+
# Detect format: v2, v1, or v0
|
|
85
|
+
if (module_dir / "module.yaml").exists():
|
|
86
|
+
fmt = "v2"
|
|
87
|
+
elif (module_dir / "MODULE.md").exists():
|
|
88
|
+
fmt = "v1"
|
|
89
|
+
elif (module_dir / "module.md").exists():
|
|
90
|
+
fmt = "v0"
|
|
91
|
+
else:
|
|
92
|
+
continue
|
|
84
93
|
|
|
85
94
|
seen.add(module_dir.name)
|
|
86
95
|
modules.append({
|
|
@@ -105,9 +114,9 @@ def install_from_local(source: Path, name: Optional[str] = None) -> Path:
|
|
|
105
114
|
if not source.exists():
|
|
106
115
|
raise FileNotFoundError(f"Source not found: {source}")
|
|
107
116
|
|
|
108
|
-
# Check for valid module (
|
|
109
|
-
if not (
|
|
110
|
-
raise ValueError(f"Not a valid module (missing MODULE.md or module.md): {source}")
|
|
117
|
+
# Check for valid module (v2, v1, or v0 format)
|
|
118
|
+
if not _is_valid_module(source):
|
|
119
|
+
raise ValueError(f"Not a valid module (missing module.yaml, MODULE.md, or module.md): {source}")
|
|
111
120
|
|
|
112
121
|
module_name = name or source.name
|
|
113
122
|
target = ensure_user_modules_dir() / module_name
|
|
@@ -116,9 +125,277 @@ def install_from_local(source: Path, name: Optional[str] = None) -> Path:
|
|
|
116
125
|
shutil.rmtree(target)
|
|
117
126
|
|
|
118
127
|
shutil.copytree(source, target)
|
|
128
|
+
|
|
129
|
+
# Record source info for update tracking
|
|
130
|
+
_record_module_source(module_name, source)
|
|
131
|
+
|
|
119
132
|
return target
|
|
120
133
|
|
|
121
134
|
|
|
135
|
+
def _record_module_source(
|
|
136
|
+
name: str,
|
|
137
|
+
source: Path,
|
|
138
|
+
github_url: str = None,
|
|
139
|
+
module_path: str = None,
|
|
140
|
+
tag: str = None,
|
|
141
|
+
branch: str = None,
|
|
142
|
+
version: str = None,
|
|
143
|
+
):
|
|
144
|
+
"""Record module source info for future updates."""
|
|
145
|
+
manifest_path = Path.home() / ".cognitive" / "installed.json"
|
|
146
|
+
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
|
|
148
|
+
# Load existing manifest
|
|
149
|
+
manifest = {}
|
|
150
|
+
if manifest_path.exists():
|
|
151
|
+
try:
|
|
152
|
+
with open(manifest_path, 'r') as f:
|
|
153
|
+
manifest = json.load(f)
|
|
154
|
+
except:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
# Get current timestamp
|
|
158
|
+
from datetime import datetime
|
|
159
|
+
now = datetime.now().isoformat()
|
|
160
|
+
|
|
161
|
+
# Update entry
|
|
162
|
+
manifest[name] = {
|
|
163
|
+
"source": str(source),
|
|
164
|
+
"github_url": github_url,
|
|
165
|
+
"module_path": module_path,
|
|
166
|
+
"tag": tag,
|
|
167
|
+
"branch": branch,
|
|
168
|
+
"version": version,
|
|
169
|
+
"installed_at": str(Path.home() / ".cognitive" / "modules" / name),
|
|
170
|
+
"installed_time": now,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
with open(manifest_path, 'w') as f:
|
|
174
|
+
json.dump(manifest, f, indent=2)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_installed_module_info(name: str) -> Optional[dict]:
|
|
178
|
+
"""Get installation info for a module."""
|
|
179
|
+
manifest_path = Path.home() / ".cognitive" / "installed.json"
|
|
180
|
+
if not manifest_path.exists():
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
with open(manifest_path, 'r') as f:
|
|
185
|
+
manifest = json.load(f)
|
|
186
|
+
return manifest.get(name)
|
|
187
|
+
except:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_module_version(module_path: Path) -> Optional[str]:
|
|
192
|
+
"""Extract version from a module's metadata."""
|
|
193
|
+
import yaml
|
|
194
|
+
|
|
195
|
+
# Try v2 format (module.yaml)
|
|
196
|
+
yaml_path = module_path / "module.yaml"
|
|
197
|
+
if yaml_path.exists():
|
|
198
|
+
try:
|
|
199
|
+
with open(yaml_path, 'r') as f:
|
|
200
|
+
data = yaml.safe_load(f)
|
|
201
|
+
return data.get("version")
|
|
202
|
+
except:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
# Try v1 format (MODULE.md with frontmatter)
|
|
206
|
+
md_path = module_path / "MODULE.md"
|
|
207
|
+
if not md_path.exists():
|
|
208
|
+
md_path = module_path / "module.md"
|
|
209
|
+
|
|
210
|
+
if md_path.exists():
|
|
211
|
+
try:
|
|
212
|
+
with open(md_path, 'r') as f:
|
|
213
|
+
content = f.read()
|
|
214
|
+
if content.startswith("---"):
|
|
215
|
+
parts = content.split("---", 2)
|
|
216
|
+
if len(parts) >= 3:
|
|
217
|
+
meta = yaml.safe_load(parts[1])
|
|
218
|
+
return meta.get("version")
|
|
219
|
+
except:
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def update_module(name: str) -> tuple[Path, str, str]:
|
|
226
|
+
"""
|
|
227
|
+
Update an installed module to the latest version.
|
|
228
|
+
|
|
229
|
+
Returns: (path, old_version, new_version)
|
|
230
|
+
"""
|
|
231
|
+
info = get_installed_module_info(name)
|
|
232
|
+
if not info:
|
|
233
|
+
raise ValueError(f"Module not installed or no source info: {name}")
|
|
234
|
+
|
|
235
|
+
github_url = info.get("github_url")
|
|
236
|
+
if not github_url:
|
|
237
|
+
raise ValueError(f"Module was not installed from GitHub, cannot update: {name}")
|
|
238
|
+
|
|
239
|
+
# Get current version
|
|
240
|
+
current_path = USER_MODULES_DIR / name
|
|
241
|
+
old_version = get_module_version(current_path) if current_path.exists() else None
|
|
242
|
+
|
|
243
|
+
# Re-install from source
|
|
244
|
+
module_path = info.get("module_path")
|
|
245
|
+
tag = info.get("tag")
|
|
246
|
+
branch = info.get("branch", "main")
|
|
247
|
+
|
|
248
|
+
# If installed with a specific tag, use that tag; otherwise use branch
|
|
249
|
+
ref = tag if tag else branch
|
|
250
|
+
|
|
251
|
+
new_path = install_from_github_url(
|
|
252
|
+
url=github_url,
|
|
253
|
+
module_path=module_path,
|
|
254
|
+
name=name,
|
|
255
|
+
tag=tag,
|
|
256
|
+
branch=branch if not tag else "main",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Get new version
|
|
260
|
+
new_version = get_module_version(new_path)
|
|
261
|
+
|
|
262
|
+
return new_path, old_version, new_version
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def install_from_github_url(
|
|
266
|
+
url: str,
|
|
267
|
+
module_path: Optional[str] = None,
|
|
268
|
+
name: Optional[str] = None,
|
|
269
|
+
branch: str = "main",
|
|
270
|
+
tag: Optional[str] = None,
|
|
271
|
+
) -> Path:
|
|
272
|
+
"""
|
|
273
|
+
Install a module from a GitHub URL without requiring git.
|
|
274
|
+
|
|
275
|
+
Uses GitHub's ZIP download feature for lightweight installation.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
url: GitHub URL or org/repo shorthand
|
|
279
|
+
module_path: Path to module within repository
|
|
280
|
+
name: Override module name
|
|
281
|
+
branch: Git branch (default: main)
|
|
282
|
+
tag: Git tag/release version (takes priority over branch)
|
|
283
|
+
|
|
284
|
+
Examples:
|
|
285
|
+
install_from_github_url("https://github.com/ziel-io/cognitive-modules",
|
|
286
|
+
module_path="cognitive/modules/code-simplifier")
|
|
287
|
+
install_from_github_url("ziel-io/cognitive-modules",
|
|
288
|
+
module_path="code-simplifier",
|
|
289
|
+
tag="v1.0.0")
|
|
290
|
+
"""
|
|
291
|
+
# Parse shorthand (org/repo)
|
|
292
|
+
original_url = url
|
|
293
|
+
if not url.startswith("http"):
|
|
294
|
+
url = f"https://github.com/{url}"
|
|
295
|
+
|
|
296
|
+
# Extract org/repo from URL
|
|
297
|
+
match = re.match(r"https://github\.com/([^/]+)/([^/]+)/?", url)
|
|
298
|
+
if not match:
|
|
299
|
+
raise ValueError(f"Invalid GitHub URL: {url}")
|
|
300
|
+
|
|
301
|
+
org, repo = match.groups()
|
|
302
|
+
repo = repo.rstrip(".git")
|
|
303
|
+
github_url = f"https://github.com/{org}/{repo}"
|
|
304
|
+
|
|
305
|
+
# Build ZIP download URL (tag takes priority over branch)
|
|
306
|
+
if tag:
|
|
307
|
+
# Try tag first, then as a release tag
|
|
308
|
+
zip_url = f"https://github.com/{org}/{repo}/archive/refs/tags/{tag}.zip"
|
|
309
|
+
else:
|
|
310
|
+
zip_url = f"https://github.com/{org}/{repo}/archive/refs/heads/{branch}.zip"
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
# Download ZIP
|
|
314
|
+
req = Request(zip_url, headers={"User-Agent": "cognitive-modules/1.0"})
|
|
315
|
+
with urlopen(req, timeout=30) as response:
|
|
316
|
+
zip_data = response.read()
|
|
317
|
+
except URLError as e:
|
|
318
|
+
if tag:
|
|
319
|
+
raise RuntimeError(f"Failed to download tag '{tag}' from GitHub: {e}")
|
|
320
|
+
raise RuntimeError(f"Failed to download from GitHub: {e}")
|
|
321
|
+
|
|
322
|
+
# Extract to temp directory
|
|
323
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
324
|
+
tmppath = Path(tmpdir)
|
|
325
|
+
|
|
326
|
+
# Extract ZIP
|
|
327
|
+
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
328
|
+
zf.extractall(tmppath)
|
|
329
|
+
|
|
330
|
+
# Find extracted directory (usually repo-branch or repo-tag)
|
|
331
|
+
extracted_dirs = list(tmppath.iterdir())
|
|
332
|
+
if not extracted_dirs:
|
|
333
|
+
raise RuntimeError("ZIP file was empty")
|
|
334
|
+
repo_root = extracted_dirs[0]
|
|
335
|
+
|
|
336
|
+
# Determine source path
|
|
337
|
+
if module_path:
|
|
338
|
+
# Try different module path patterns
|
|
339
|
+
source = None
|
|
340
|
+
possible_paths = [
|
|
341
|
+
repo_root / module_path,
|
|
342
|
+
repo_root / "cognitive" / "modules" / module_path,
|
|
343
|
+
repo_root / "modules" / module_path,
|
|
344
|
+
]
|
|
345
|
+
for p in possible_paths:
|
|
346
|
+
if p.exists() and _is_valid_module(p):
|
|
347
|
+
source = p
|
|
348
|
+
break
|
|
349
|
+
|
|
350
|
+
if not source:
|
|
351
|
+
raise FileNotFoundError(
|
|
352
|
+
f"Module not found at: {module_path}\n"
|
|
353
|
+
f"Searched in: {[str(p.relative_to(repo_root)) for p in possible_paths]}"
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
# Use repo root as module
|
|
357
|
+
source = repo_root
|
|
358
|
+
if not _is_valid_module(source):
|
|
359
|
+
raise ValueError(
|
|
360
|
+
f"Repository root is not a valid module. "
|
|
361
|
+
f"Use --module to specify the module path."
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Determine module name and version
|
|
365
|
+
module_name = name or source.name
|
|
366
|
+
version = get_module_version(source)
|
|
367
|
+
|
|
368
|
+
# Install to user modules dir
|
|
369
|
+
target = ensure_user_modules_dir() / module_name
|
|
370
|
+
|
|
371
|
+
if target.exists():
|
|
372
|
+
shutil.rmtree(target)
|
|
373
|
+
|
|
374
|
+
shutil.copytree(source, target)
|
|
375
|
+
|
|
376
|
+
# Record source info for future updates
|
|
377
|
+
_record_module_source(
|
|
378
|
+
name=module_name,
|
|
379
|
+
source=source,
|
|
380
|
+
github_url=github_url,
|
|
381
|
+
module_path=module_path,
|
|
382
|
+
tag=tag,
|
|
383
|
+
branch=branch,
|
|
384
|
+
version=version,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
return target
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _is_valid_module(path: Path) -> bool:
|
|
391
|
+
"""Check if a directory is a valid cognitive module."""
|
|
392
|
+
return (
|
|
393
|
+
(path / "module.yaml").exists() or # v2 format
|
|
394
|
+
(path / "MODULE.md").exists() or # v1 format
|
|
395
|
+
(path / "module.md").exists() # v0 format
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
122
399
|
def install_from_git(url: str, subdir: Optional[str] = None, name: Optional[str] = None) -> Path:
|
|
123
400
|
"""
|
|
124
401
|
Install a module from a git repository.
|
|
@@ -274,3 +551,40 @@ def search_registry(query: str) -> list[dict]:
|
|
|
274
551
|
})
|
|
275
552
|
|
|
276
553
|
return results
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def list_github_tags(url: str, limit: int = 10) -> list[str]:
|
|
557
|
+
"""
|
|
558
|
+
List available tags/releases from a GitHub repository.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
url: GitHub URL or org/repo shorthand
|
|
562
|
+
limit: Maximum number of tags to return
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
List of tag names (e.g., ["v1.2.0", "v1.1.0", "v1.0.0"])
|
|
566
|
+
"""
|
|
567
|
+
# Parse shorthand
|
|
568
|
+
if not url.startswith("http"):
|
|
569
|
+
url = f"https://github.com/{url}"
|
|
570
|
+
|
|
571
|
+
match = re.match(r"https://github\.com/([^/]+)/([^/]+)/?", url)
|
|
572
|
+
if not match:
|
|
573
|
+
raise ValueError(f"Invalid GitHub URL: {url}")
|
|
574
|
+
|
|
575
|
+
org, repo = match.groups()
|
|
576
|
+
repo = repo.rstrip(".git")
|
|
577
|
+
|
|
578
|
+
# Use GitHub API to get tags
|
|
579
|
+
api_url = f"https://api.github.com/repos/{org}/{repo}/tags?per_page={limit}"
|
|
580
|
+
|
|
581
|
+
try:
|
|
582
|
+
req = Request(api_url, headers={
|
|
583
|
+
"User-Agent": "cognitive-modules/1.0",
|
|
584
|
+
"Accept": "application/vnd.github.v3+json",
|
|
585
|
+
})
|
|
586
|
+
with urlopen(req, timeout=10) as response:
|
|
587
|
+
data = json.loads(response.read().decode())
|
|
588
|
+
return [tag["name"] for tag in data]
|
|
589
|
+
except URLError as e:
|
|
590
|
+
return []
|