agentops-cockpit 0.9.7__py3-none-any.whl → 0.9.8__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.
- agent_ops_cockpit/agent.py +43 -81
- agent_ops_cockpit/cache/semantic_cache.py +10 -21
- agent_ops_cockpit/cli/main.py +105 -153
- agent_ops_cockpit/eval/load_test.py +33 -50
- agent_ops_cockpit/eval/quality_climber.py +88 -93
- agent_ops_cockpit/eval/red_team.py +54 -21
- agent_ops_cockpit/mcp_server.py +26 -93
- agent_ops_cockpit/ops/arch_review.py +221 -148
- agent_ops_cockpit/ops/auditors/base.py +50 -0
- agent_ops_cockpit/ops/auditors/behavioral.py +31 -0
- agent_ops_cockpit/ops/auditors/compliance.py +35 -0
- agent_ops_cockpit/ops/auditors/dependency.py +48 -0
- agent_ops_cockpit/ops/auditors/finops.py +48 -0
- agent_ops_cockpit/ops/auditors/graph.py +49 -0
- agent_ops_cockpit/ops/auditors/pivot.py +51 -0
- agent_ops_cockpit/ops/auditors/reasoning.py +67 -0
- agent_ops_cockpit/ops/auditors/reliability.py +53 -0
- agent_ops_cockpit/ops/auditors/security.py +87 -0
- agent_ops_cockpit/ops/auditors/sme_v12.py +76 -0
- agent_ops_cockpit/ops/auditors/sovereignty.py +74 -0
- agent_ops_cockpit/ops/auditors/sre_a2a.py +179 -0
- agent_ops_cockpit/ops/benchmarker.py +97 -0
- agent_ops_cockpit/ops/cost_optimizer.py +15 -24
- agent_ops_cockpit/ops/discovery.py +214 -0
- agent_ops_cockpit/ops/evidence_bridge.py +30 -63
- agent_ops_cockpit/ops/frameworks.py +124 -1
- agent_ops_cockpit/ops/git_portal.py +74 -0
- agent_ops_cockpit/ops/mcp_hub.py +19 -42
- agent_ops_cockpit/ops/orchestrator.py +477 -277
- agent_ops_cockpit/ops/policy_engine.py +38 -38
- agent_ops_cockpit/ops/reliability.py +120 -65
- agent_ops_cockpit/ops/remediator.py +54 -0
- agent_ops_cockpit/ops/secret_scanner.py +34 -22
- agent_ops_cockpit/ops/swarm.py +17 -27
- agent_ops_cockpit/ops/ui_auditor.py +67 -6
- agent_ops_cockpit/ops/watcher.py +41 -70
- agent_ops_cockpit/ops/watchlist.json +30 -0
- agent_ops_cockpit/optimizer.py +157 -407
- agent_ops_cockpit/tests/test_arch_review.py +6 -6
- agent_ops_cockpit/tests/test_discovery.py +96 -0
- agent_ops_cockpit/tests/test_ops_core.py +56 -0
- agent_ops_cockpit/tests/test_orchestrator_fleet.py +73 -0
- agent_ops_cockpit/tests/test_persona_architect.py +75 -0
- agent_ops_cockpit/tests/test_persona_finops.py +31 -0
- agent_ops_cockpit/tests/test_persona_security.py +55 -0
- agent_ops_cockpit/tests/test_persona_sre.py +43 -0
- agent_ops_cockpit/tests/test_persona_ux.py +42 -0
- agent_ops_cockpit/tests/test_quality_climber.py +2 -2
- agent_ops_cockpit/tests/test_remediator.py +75 -0
- agent_ops_cockpit/tests/test_ui_auditor.py +52 -0
- agentops_cockpit-0.9.8.dist-info/METADATA +172 -0
- agentops_cockpit-0.9.8.dist-info/RECORD +71 -0
- agent_ops_cockpit/tests/test_optimizer.py +0 -68
- agent_ops_cockpit/tests/test_red_team.py +0 -35
- agent_ops_cockpit/tests/test_secret_scanner.py +0 -24
- agentops_cockpit-0.9.7.dist-info/METADATA +0 -246
- agentops_cockpit-0.9.7.dist-info/RECORD +0 -47
- {agentops_cockpit-0.9.7.dist-info → agentops_cockpit-0.9.8.dist-info}/WHEEL +0 -0
- {agentops_cockpit-0.9.7.dist-info → agentops_cockpit-0.9.8.dist-info}/entry_points.txt +0 -0
- {agentops_cockpit-0.9.7.dist-info → agentops_cockpit-0.9.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,7 +9,7 @@ app = typer.Typer(help="Face Auditor: Scan frontend code for A2UI alignment.")
|
|
|
9
9
|
console = Console()
|
|
10
10
|
|
|
11
11
|
@app.command()
|
|
12
|
-
def audit(path: str = "src"):
|
|
12
|
+
def audit(path: str = typer.Argument("src", help="Directory to scan")):
|
|
13
13
|
"""
|
|
14
14
|
Step 4: Frontend / A2UI Auditing.
|
|
15
15
|
Ensures frontend components are properly mapping surfaceId and detecting triggers.
|
|
@@ -28,13 +28,19 @@ def audit(path: str = "src"):
|
|
|
28
28
|
a11y_pattern = re.compile(r"aria-label|role=|tabIndex|alt=")
|
|
29
29
|
legal_pattern = re.compile(r"Copyright|PrivacyPolicy|Disclaimer|TermsOfService|©")
|
|
30
30
|
marketing_pattern = re.compile(r"og:image|meta\s+name=['\"]description['\"]|favicon|logo")
|
|
31
|
+
hitl_pattern = re.compile(r"HumanInTheLoop|confirm|Approve|Reject|Gate")
|
|
32
|
+
streaming_pattern = re.compile(r"Suspense|Stream|Markdown|chunk")
|
|
31
33
|
|
|
32
34
|
for root, dirs, files in os.walk(path):
|
|
33
35
|
if any(d in root for d in [".venv", "node_modules", ".git", "dist"]):
|
|
34
36
|
continue
|
|
35
37
|
|
|
36
38
|
for file in files:
|
|
39
|
+
# Skip non-component files to reduce noise
|
|
37
40
|
if file.endswith((".tsx", ".ts", ".js", ".jsx")):
|
|
41
|
+
if any(x in file.lower() for x in ["config", "test", "spec", "d.ts", "setup", "main", "index"]):
|
|
42
|
+
continue
|
|
43
|
+
|
|
38
44
|
files_scanned += 1
|
|
39
45
|
file_path = os.path.join(root, file)
|
|
40
46
|
rel_path = os.path.relpath(file_path, ".")
|
|
@@ -78,15 +84,59 @@ def audit(path: str = "src"):
|
|
|
78
84
|
if not marketing_pattern.search(content) and ("index" in file.lower() or "head" in file.lower() or "App" in file):
|
|
79
85
|
findings.append({"line": 1, "issue": "Missing Branding (Logo) or SEO Metadata (OG/Description)", "fix": "Add meta tags (og:image, description) and project logo."})
|
|
80
86
|
|
|
87
|
+
if not hitl_pattern.search(content) and ("Action" in file or "Tool" in file or "Transfer" in file):
|
|
88
|
+
findings.append({"line": 1, "issue": "Missing HITL (Human-in-the-Loop) Gating", "fix": "Add confirmation modals or 'Approve/Reject' gates for high-impact actions."})
|
|
89
|
+
|
|
90
|
+
if not streaming_pattern.search(content) and ("Chat" in file or "Thread" in file or "Log" in file):
|
|
91
|
+
findings.append({"line": 1, "issue": "Missing Streaming Resilience (Suspense/Stream)", "fix": "Implement Suspense or stream-aware handlers to prevent UI flickering during token rendering."})
|
|
92
|
+
|
|
81
93
|
if findings:
|
|
82
94
|
issues.append({"file": rel_path, "findings": findings})
|
|
83
95
|
|
|
84
96
|
except Exception:
|
|
85
97
|
pass
|
|
86
98
|
|
|
99
|
+
# Quantitative Scoring (Principal v1.2)
|
|
100
|
+
max_score = 100
|
|
101
|
+
deductions = {
|
|
102
|
+
"Missing 'surfaceId'": 20,
|
|
103
|
+
"Missing 'Thinking' feedback": 15,
|
|
104
|
+
"Missing HITL": 15,
|
|
105
|
+
"Missing Streaming Resilience": 10,
|
|
106
|
+
"interactive component without triggers": 10,
|
|
107
|
+
"Missing a11y labels": 10,
|
|
108
|
+
"Missing Legal/SEO": 5
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
total_deduction = 0
|
|
112
|
+
unique_issues = set()
|
|
113
|
+
for issue in issues:
|
|
114
|
+
for finding in issue["findings"]:
|
|
115
|
+
for key, val in deductions.items():
|
|
116
|
+
if key.lower() in finding["issue"].lower():
|
|
117
|
+
if key not in unique_issues:
|
|
118
|
+
total_deduction += val
|
|
119
|
+
unique_issues.add(key)
|
|
120
|
+
|
|
121
|
+
score = max(0, max_score - total_deduction)
|
|
122
|
+
verdict = "✅ APPROVED" if score >= 85 else "⚠️ WARN" if score >= 60 else "❌ REJECTED"
|
|
123
|
+
|
|
87
124
|
console.print(f"📝 Scanned [bold]{files_scanned}[/bold] frontend files.")
|
|
88
125
|
|
|
89
|
-
|
|
126
|
+
# Executive Summary Table (Product View v1.2)
|
|
127
|
+
summary = Table(title="💎 PRINCIPAL UX EVALUATION (v1.2)", box=None)
|
|
128
|
+
summary.add_column("Metric", style="cyan")
|
|
129
|
+
summary.add_column("Value", style="bold white")
|
|
130
|
+
summary.add_row("GenUI Readiness Score", f"{score}/100")
|
|
131
|
+
summary.add_row("Consensus Verdict", f"[{'green' if score >= 85 else 'yellow' if score >= 60 else 'red'}]{verdict}[/]")
|
|
132
|
+
summary.add_row("A2UI Registry Depth", "Fragmented" if "Missing 'surfaceId'" in unique_issues else "Aligned")
|
|
133
|
+
summary.add_row("Latency Tolerance", "Low" if "Missing 'Thinking' feedback" in unique_issues else "Premium")
|
|
134
|
+
summary.add_row("Autonomous Risk (HITL)", "HIGH" if "Missing HITL" in unique_issues else "Secured")
|
|
135
|
+
summary.add_row("Streaming Fluidity", "Flicker-Prone" if "Missing Streaming Resilience" in unique_issues else "Smooth")
|
|
136
|
+
|
|
137
|
+
console.print(Panel(summary, border_style="green" if score >= 85 else "yellow"))
|
|
138
|
+
|
|
139
|
+
table = Table(title="🔍 A2UI DETAILED FINDINGS")
|
|
90
140
|
table.add_column("File:Line", style="cyan")
|
|
91
141
|
table.add_column("Issue", style="red")
|
|
92
142
|
table.add_column("Recommended Fix", style="green")
|
|
@@ -104,12 +154,23 @@ def audit(path: str = "src"):
|
|
|
104
154
|
|
|
105
155
|
console.print("\n", table)
|
|
106
156
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
157
|
+
if score < 85:
|
|
158
|
+
console.print(f"\n💡 [bold yellow]UX Principal Recommendation:[/bold yellow] Your 'Face' layer needs {100-score}% more alignment.")
|
|
159
|
+
if "Missing 'surfaceId'" in unique_issues:
|
|
160
|
+
console.print(" - Map components to 'surfaceId' to enable agent-driven UI updates.")
|
|
161
|
+
if "Missing 'Thinking' feedback" in unique_issues:
|
|
162
|
+
console.print(" - Add 'Skeleton' screens to mask LLM reasoning latency.")
|
|
163
|
+
if "Missing HITL" in unique_issues:
|
|
164
|
+
console.print(" - Implement Human-in-the-Loop gates (modals/approvals) for sensitive impacts.")
|
|
165
|
+
if "Missing Streaming Resilience" in unique_issues:
|
|
166
|
+
console.print(" - Use 'Suspense' boundaries to handle live token streaming without flicker.")
|
|
111
167
|
else:
|
|
112
168
|
console.print("\n✅ [bold green]Frontend is Well-Architected for GenUI interactions.[/bold green]")
|
|
113
169
|
|
|
170
|
+
@app.command()
|
|
171
|
+
def version():
|
|
172
|
+
"""Show the version of the Face Auditor."""
|
|
173
|
+
console.print('[bold cyan]v1.3.0[/bold cyan]')
|
|
174
|
+
|
|
114
175
|
if __name__ == "__main__":
|
|
115
176
|
app()
|
agent_ops_cockpit/ops/watcher.py
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
from tenacity import retry, wait_exponential, stop_after_attempt
|
|
2
|
+
from tenacity import retry, wait_exponential, stop_after_attempt
|
|
3
|
+
from tenacity import retry, wait_exponential, stop_after_attempt
|
|
4
|
+
from tenacity import retry, wait_exponential, stop_after_attempt
|
|
5
|
+
from tenacity import retry, wait_exponential, stop_after_attempt
|
|
1
6
|
import json
|
|
2
7
|
import os
|
|
3
8
|
import urllib.request
|
|
@@ -11,21 +16,18 @@ from packaging import version
|
|
|
11
16
|
from rich.console import Console
|
|
12
17
|
from rich.table import Table
|
|
13
18
|
from rich.panel import Panel
|
|
14
|
-
|
|
15
19
|
console = Console()
|
|
16
|
-
|
|
17
|
-
WATCHLIST_PATH = os.path.join(os.path.dirname(__file__), "watchlist.json")
|
|
20
|
+
WATCHLIST_PATH = os.path.join(os.path.dirname(__file__), 'watchlist.json')
|
|
18
21
|
|
|
19
22
|
def get_local_version(package_name: str) -> str:
|
|
20
23
|
try:
|
|
21
24
|
return importlib.metadata.version(package_name)
|
|
22
25
|
except importlib.metadata.PackageNotFoundError:
|
|
23
|
-
return
|
|
26
|
+
return 'Not Installed'
|
|
24
27
|
|
|
25
28
|
def clean_version(v_str: str) -> str:
|
|
26
29
|
"""Extracts a clean version number from strings like 'v1.2.3', 'package==1.2.3', '2026-01-28 (v0.1.0)'"""
|
|
27
|
-
|
|
28
|
-
match = re.search(r'(\d+\.\d+(?:\.\d+)?(?:[a-zA-Z]+\d+)?)', v_str)
|
|
30
|
+
match = re.search('(\\d+\\.\\d+(?:\\.\\d+)?(?:[a-zA-Z]+\\d+)?)', v_str)
|
|
29
31
|
if match:
|
|
30
32
|
return match.group(1)
|
|
31
33
|
return v_str.strip().lstrip('v')
|
|
@@ -37,102 +39,71 @@ def fetch_latest_from_atom(url: str) -> Optional[Dict[str, str]]:
|
|
|
37
39
|
tree = ET.parse(response)
|
|
38
40
|
root = tree.getroot()
|
|
39
41
|
ns = {'ns': 'http://www.w3.org/2005/Atom'}
|
|
40
|
-
|
|
41
42
|
latest_entry = root.find('ns:entry', ns)
|
|
42
43
|
if latest_entry is not None:
|
|
43
44
|
title = latest_entry.find('ns:title', ns).text
|
|
44
45
|
updated = latest_entry.find('ns:updated', ns).text
|
|
45
46
|
raw_v = title.strip().split()[-1]
|
|
46
|
-
return {
|
|
47
|
-
"version": clean_version(raw_v) if "==" not in raw_v else clean_version(raw_v.split("==")[-1]),
|
|
48
|
-
"date": updated,
|
|
49
|
-
"title": title
|
|
50
|
-
}
|
|
47
|
+
return {'version': clean_version(raw_v) if '==' not in raw_v else clean_version(raw_v.split('==')[-1]), 'date': updated, 'title': title}
|
|
51
48
|
except Exception:
|
|
52
|
-
# console.print(f"[dim red]Error fetching {url}: {e}[/dim red]")
|
|
53
49
|
return None
|
|
54
50
|
return None
|
|
55
51
|
|
|
56
52
|
def run_watch():
|
|
57
|
-
console.print(Panel.fit(
|
|
58
|
-
|
|
53
|
+
console.print(Panel.fit('🔍 [bold blue]AGENTOPS COCKPIT: ECOSYSTEM WATCHER[/bold blue]', border_style='blue'))
|
|
59
54
|
if not os.path.exists(WATCHLIST_PATH):
|
|
60
|
-
console.print(f
|
|
55
|
+
console.print(f'❌ [red]Watchlist not found at {WATCHLIST_PATH}[/red]')
|
|
61
56
|
return
|
|
62
|
-
|
|
63
57
|
with open(WATCHLIST_PATH, 'r') as f:
|
|
64
58
|
watchlist = json.load(f)
|
|
65
|
-
|
|
66
|
-
table
|
|
67
|
-
table.add_column(
|
|
68
|
-
table.add_column(
|
|
69
|
-
table.add_column(
|
|
70
|
-
table.add_column(
|
|
71
|
-
table.add_column("Status", justify="center")
|
|
72
|
-
|
|
59
|
+
table = Table(title=f"Ecosystem Pulse - {datetime.now().strftime('%Y-%m-%d')}", show_header=True, header_style='bold magenta')
|
|
60
|
+
table.add_column('Category', style='cyan')
|
|
61
|
+
table.add_column('Component', style='white')
|
|
62
|
+
table.add_column('Local', style='yellow')
|
|
63
|
+
table.add_column('Latest', style='green')
|
|
64
|
+
table.add_column('Status', justify='center')
|
|
73
65
|
updates_found = []
|
|
74
|
-
|
|
75
66
|
for category, items in watchlist.items():
|
|
67
|
+
if not isinstance(items, dict):
|
|
68
|
+
continue
|
|
76
69
|
for name, info in items.items():
|
|
77
|
-
|
|
70
|
+
if not isinstance(info, dict) or 'feed' not in info:
|
|
71
|
+
continue
|
|
72
|
+
package = info.get('package')
|
|
78
73
|
local_v_raw = get_local_version(package) if package else None
|
|
79
|
-
local_v = local_v_raw if local_v_raw else
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
latest_info = fetch_latest_from_atom(info["feed"])
|
|
83
|
-
|
|
74
|
+
local_v = local_v_raw if local_v_raw else 'N/A'
|
|
75
|
+
with console.status(f'[dim]Checking {name}...'):
|
|
76
|
+
latest_info = fetch_latest_from_atom(info['feed'])
|
|
84
77
|
if latest_info:
|
|
85
|
-
latest_v = latest_info[
|
|
86
|
-
|
|
78
|
+
latest_v = latest_info['version']
|
|
87
79
|
is_outdated = False
|
|
88
|
-
if local_v_raw and local_v_raw !=
|
|
80
|
+
if local_v_raw and local_v_raw != 'Not Installed':
|
|
89
81
|
try:
|
|
90
82
|
is_outdated = version.parse(latest_v) > version.parse(local_v_raw)
|
|
91
83
|
except Exception:
|
|
92
84
|
is_outdated = latest_v > local_v_raw
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
status = "➕ [dim]NEW[/dim]"
|
|
85
|
+
status = '🚨 [bold red]UPDATE[/bold red]' if is_outdated else '✅ [green]OK[/green]'
|
|
86
|
+
if local_v == 'Not Installed':
|
|
87
|
+
status = '➕ [dim]NEW[/dim]'
|
|
97
88
|
if package is None:
|
|
98
|
-
status =
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
table.add_row(
|
|
102
|
-
category.upper(),
|
|
103
|
-
name,
|
|
104
|
-
display_local,
|
|
105
|
-
latest_v,
|
|
106
|
-
status
|
|
107
|
-
)
|
|
108
|
-
|
|
89
|
+
status = '🌐 [blue]SPEC[/blue]'
|
|
90
|
+
display_local = local_v if local_v != 'Not Installed' else '[dim]Not Installed[/dim]'
|
|
91
|
+
table.add_row(category.upper(), name, display_local, latest_v, status)
|
|
109
92
|
if is_outdated:
|
|
110
|
-
updates_found.append({
|
|
111
|
-
"name": name,
|
|
112
|
-
"current": local_v,
|
|
113
|
-
"latest": latest_v,
|
|
114
|
-
"package": package,
|
|
115
|
-
"desc": info["description"]
|
|
116
|
-
})
|
|
93
|
+
updates_found.append({'name': name, 'current': local_v, 'latest': latest_v, 'package': package, 'desc': info['description']})
|
|
117
94
|
else:
|
|
118
|
-
table.add_row(category.upper(), name, local_v,
|
|
119
|
-
|
|
95
|
+
table.add_row(category.upper(), name, local_v, '[red]Fetch Failed[/red]', '❓')
|
|
120
96
|
console.print(table)
|
|
121
|
-
|
|
122
97
|
if updates_found:
|
|
123
|
-
console.print(
|
|
98
|
+
console.print('\n[bold yellow]⚠️ Actionable Intelligence:[/bold yellow]')
|
|
124
99
|
for up in updates_found:
|
|
125
100
|
console.print(f"• [bold]{up['name']}[/bold]: {up['current']} ➔ [bold green]{up['latest']}[/bold green]")
|
|
126
101
|
console.print(f" [dim]{up['desc']}[/dim]")
|
|
127
|
-
|
|
128
|
-
pkgs = " ".join([u['package'] for u in updates_found if u['package']])
|
|
102
|
+
pkgs = ' '.join([u['package'] for u in updates_found if u['package']])
|
|
129
103
|
if pkgs:
|
|
130
|
-
console.print(f
|
|
131
|
-
|
|
132
|
-
# Exit with special code to signal updates to CI/CD
|
|
104
|
+
console.print(f'\n[bold cyan]Pro-tip:[/bold cyan] Run `pip install --upgrade {pkgs}` to sync.')
|
|
133
105
|
sys.exit(2)
|
|
134
106
|
else:
|
|
135
|
-
console.print(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
run_watch()
|
|
107
|
+
console.print('\n[bold green]✨ All components are currently in sync with the latest stable releases.[/bold green]')
|
|
108
|
+
if __name__ == '__main__':
|
|
109
|
+
run_watch()
|
|
@@ -43,6 +43,36 @@
|
|
|
43
43
|
"package": "google-cloud-aiplatform",
|
|
44
44
|
"description": "Vertex AI Agent Engine - Managed agent execution",
|
|
45
45
|
"min_version_for_optimizations": "1.70.0"
|
|
46
|
+
},
|
|
47
|
+
"pinecone": {
|
|
48
|
+
"feed": "https://github.com/pinecone-io/pinecone-python-client/releases.atom",
|
|
49
|
+
"package": "pinecone-client",
|
|
50
|
+
"description": "Pinecone - Managed vector database for RAG"
|
|
51
|
+
},
|
|
52
|
+
"weaviate": {
|
|
53
|
+
"feed": "https://github.com/weaviate/weaviate-python-client/releases.atom",
|
|
54
|
+
"package": "weaviate-client",
|
|
55
|
+
"description": "Weaviate - Open source vector database"
|
|
56
|
+
},
|
|
57
|
+
"chromadb": {
|
|
58
|
+
"feed": "https://github.com/chroma-core/chroma/releases.atom",
|
|
59
|
+
"package": "chromadb",
|
|
60
|
+
"description": "Chroma - The AI-native open-source embedding database"
|
|
61
|
+
},
|
|
62
|
+
"llamaindex": {
|
|
63
|
+
"feed": "https://github.com/run-llama/llama_index/releases.atom",
|
|
64
|
+
"package": "llama-index",
|
|
65
|
+
"description": "LlamaIndex - Data framework for LLM applications"
|
|
66
|
+
},
|
|
67
|
+
"langsmith": {
|
|
68
|
+
"feed": "https://github.com/langchain-ai/langsmith-sdk/releases.atom",
|
|
69
|
+
"package": "langsmith",
|
|
70
|
+
"description": "LangSmith - Debugging, testing, and monitoring for LLMs"
|
|
71
|
+
},
|
|
72
|
+
"ollama": {
|
|
73
|
+
"feed": "https://github.com/ollama/ollama/releases.atom",
|
|
74
|
+
"package": "ollama",
|
|
75
|
+
"description": "Ollama - Run LLMs locally"
|
|
46
76
|
}
|
|
47
77
|
},
|
|
48
78
|
"compatibility_rules": [
|