ctxos-cli 0.1.0__tar.gz
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.
- ctxos_cli-0.1.0/.gitignore +50 -0
- ctxos_cli-0.1.0/PKG-INFO +112 -0
- ctxos_cli-0.1.0/README.md +88 -0
- ctxos_cli-0.1.0/context_cli/__init__.py +7 -0
- ctxos_cli-0.1.0/context_cli/main.py +384 -0
- ctxos_cli-0.1.0/pyproject.toml +60 -0
- ctxos_cli-0.1.0/tests/__init__.py +0 -0
- ctxos_cli-0.1.0/tests/test_cli.py +78 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Environment secrets
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
.env.local
|
|
5
|
+
.env.development.local
|
|
6
|
+
.env.test.local
|
|
7
|
+
.env.production.local
|
|
8
|
+
*.pem
|
|
9
|
+
*.key
|
|
10
|
+
|
|
11
|
+
# Dependencies
|
|
12
|
+
node_modules/
|
|
13
|
+
.pnp
|
|
14
|
+
.pnp.js
|
|
15
|
+
|
|
16
|
+
# Build output
|
|
17
|
+
.next/
|
|
18
|
+
out/
|
|
19
|
+
build/
|
|
20
|
+
dist/
|
|
21
|
+
|
|
22
|
+
# Testing
|
|
23
|
+
coverage/
|
|
24
|
+
|
|
25
|
+
# Debug logs
|
|
26
|
+
npm-debug.log*
|
|
27
|
+
yarn-debug.log*
|
|
28
|
+
yarn-error.log*
|
|
29
|
+
pnpm-debug.log*
|
|
30
|
+
|
|
31
|
+
# OS files
|
|
32
|
+
.DS_Store
|
|
33
|
+
Thumbs.db
|
|
34
|
+
*.swp
|
|
35
|
+
*.swo
|
|
36
|
+
*~
|
|
37
|
+
|
|
38
|
+
# IDE
|
|
39
|
+
.vscode/
|
|
40
|
+
.idea/
|
|
41
|
+
|
|
42
|
+
# Python
|
|
43
|
+
__pycache__/
|
|
44
|
+
*.pyc
|
|
45
|
+
*.pyo
|
|
46
|
+
.venv/
|
|
47
|
+
venv/
|
|
48
|
+
|
|
49
|
+
# Docker
|
|
50
|
+
docker-compose.override.yml
|
ctxos_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ctxos-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for Context OS — Memory, Search, Crawl, Knowledge
|
|
5
|
+
Project-URL: Homepage, https://github.com/AmanSagar0607/Context-OS
|
|
6
|
+
Project-URL: Repository, https://github.com/AmanSagar0607/Context-OS
|
|
7
|
+
Author-email: Aman Sagar <aman@amansagar.in>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: ai,cli,context,crawl,knowledge-graph,mcp,memory,search
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: click>=8.0.0
|
|
17
|
+
Requires-Dist: httpx>=0.27.0
|
|
18
|
+
Requires-Dist: pydantic>=2.0.0
|
|
19
|
+
Requires-Dist: rich>=13.0.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Context OS — CLI
|
|
26
|
+
|
|
27
|
+
Command-line interface for Context OS operations.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install context-cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or install from source:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/AmanSagar0607/Context-OS.git
|
|
39
|
+
cd Context-OS/cli
|
|
40
|
+
pip install -e .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
Set environment variables:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
export CONTEXT_API_URL="http://localhost:8000" # API server
|
|
49
|
+
export CONTEXT_API_KEY="your-api-key" # API key (optional)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Commands
|
|
53
|
+
|
|
54
|
+
### Memory
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Add a memory
|
|
58
|
+
context memory add -c "User prefers dark mode" --type semantic --importance high --tags "ui,preferences"
|
|
59
|
+
|
|
60
|
+
# Get a memory
|
|
61
|
+
context memory get <memory-id>
|
|
62
|
+
|
|
63
|
+
# Search memories
|
|
64
|
+
context memory search "dark mode" --limit 10
|
|
65
|
+
|
|
66
|
+
# List memories
|
|
67
|
+
context memory list --type semantic --limit 20
|
|
68
|
+
|
|
69
|
+
# Delete a memory
|
|
70
|
+
context memory delete <memory-id>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Search
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Web search
|
|
77
|
+
context search web "AI news 2025" --limit 5
|
|
78
|
+
|
|
79
|
+
# Internal hybrid search
|
|
80
|
+
context search internal "user preferences" --limit 10
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Crawl
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Scrape a URL
|
|
87
|
+
context crawl scrape https://example.com
|
|
88
|
+
|
|
89
|
+
# Map a website
|
|
90
|
+
context crawl map https://example.com --limit 50
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Knowledge
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Create an entity
|
|
97
|
+
context knowledge create-entity --name "GPT-4" --type model --description "Large language model"
|
|
98
|
+
|
|
99
|
+
# Search entities
|
|
100
|
+
context knowledge search "language models" --limit 10
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Health
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Check API health
|
|
107
|
+
context health
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT License
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Context OS — CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for Context OS operations.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install context-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install from source:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/AmanSagar0607/Context-OS.git
|
|
15
|
+
cd Context-OS/cli
|
|
16
|
+
pip install -e .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
Set environment variables:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export CONTEXT_API_URL="http://localhost:8000" # API server
|
|
25
|
+
export CONTEXT_API_KEY="your-api-key" # API key (optional)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Commands
|
|
29
|
+
|
|
30
|
+
### Memory
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Add a memory
|
|
34
|
+
context memory add -c "User prefers dark mode" --type semantic --importance high --tags "ui,preferences"
|
|
35
|
+
|
|
36
|
+
# Get a memory
|
|
37
|
+
context memory get <memory-id>
|
|
38
|
+
|
|
39
|
+
# Search memories
|
|
40
|
+
context memory search "dark mode" --limit 10
|
|
41
|
+
|
|
42
|
+
# List memories
|
|
43
|
+
context memory list --type semantic --limit 20
|
|
44
|
+
|
|
45
|
+
# Delete a memory
|
|
46
|
+
context memory delete <memory-id>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Search
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Web search
|
|
53
|
+
context search web "AI news 2025" --limit 5
|
|
54
|
+
|
|
55
|
+
# Internal hybrid search
|
|
56
|
+
context search internal "user preferences" --limit 10
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Crawl
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Scrape a URL
|
|
63
|
+
context crawl scrape https://example.com
|
|
64
|
+
|
|
65
|
+
# Map a website
|
|
66
|
+
context crawl map https://example.com --limit 50
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Knowledge
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Create an entity
|
|
73
|
+
context knowledge create-entity --name "GPT-4" --type model --description "Large language model"
|
|
74
|
+
|
|
75
|
+
# Search entities
|
|
76
|
+
context knowledge search "language models" --limit 10
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Health
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Check API health
|
|
83
|
+
context health
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT License
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context OS — CLI Main
|
|
3
|
+
|
|
4
|
+
Entry point for the context command.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich import print as rprint
|
|
18
|
+
|
|
19
|
+
from . import __version__
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
API_URL = os.getenv("CONTEXT_API_URL", os.getenv("CONTEXT_OS_URL", "http://localhost:8000"))
|
|
24
|
+
API_KEY = os.getenv("CONTEXT_API_KEY", os.getenv("CONTEXT_OS_API_KEY"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_client():
|
|
28
|
+
"""Get HTTP client for API calls."""
|
|
29
|
+
import httpx
|
|
30
|
+
|
|
31
|
+
headers = {"Content-Type": "application/json"}
|
|
32
|
+
if API_KEY:
|
|
33
|
+
headers["Authorization"] = f"Bearer {API_KEY}"
|
|
34
|
+
|
|
35
|
+
return httpx.Client(
|
|
36
|
+
base_url=API_URL,
|
|
37
|
+
headers=headers,
|
|
38
|
+
timeout=30.0,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@click.group()
|
|
43
|
+
@click.version_option(version=__version__, prog_name="context-cli")
|
|
44
|
+
@click.option("--url", envvar="CONTEXT_API_URL", default="http://localhost:8000", help="API server URL")
|
|
45
|
+
@click.option("--key", envvar="CONTEXT_API_KEY", default=None, help="API key")
|
|
46
|
+
@click.pass_context
|
|
47
|
+
def cli(ctx: click.Context, url: str, key: Optional[str]):
|
|
48
|
+
"""Context OS — AI Agent Infrastructure CLI"""
|
|
49
|
+
ctx.ensure_object(dict)
|
|
50
|
+
ctx.obj["url"] = url
|
|
51
|
+
ctx.obj["key"] = key
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# --- Memory Commands ---
|
|
55
|
+
|
|
56
|
+
@cli.group()
|
|
57
|
+
def memory():
|
|
58
|
+
"""Memory operations"""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@memory.command("add")
|
|
63
|
+
@click.option("--content", "-c", required=True, help="Memory content")
|
|
64
|
+
@click.option("--type", "memory_type", default="episodic", help="Memory type (episodic/semantic/procedural)")
|
|
65
|
+
@click.option("--importance", default="medium", help="Importance (low/medium/high/critical)")
|
|
66
|
+
@click.option("--tags", help="Comma-separated tags")
|
|
67
|
+
@click.pass_context
|
|
68
|
+
def memory_add(ctx: click.Context, content: str, memory_type: str, importance: str, tags: Optional[str]):
|
|
69
|
+
"""Add a new memory"""
|
|
70
|
+
tag_list = [t.strip() for t in tags.split(",")] if tags else []
|
|
71
|
+
|
|
72
|
+
payload = {
|
|
73
|
+
"content": content,
|
|
74
|
+
"memory_type": memory_type,
|
|
75
|
+
"importance": importance,
|
|
76
|
+
"tags": tag_list,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
with _get_client() as client:
|
|
80
|
+
response = client.post("/api/v1/memory", json=payload)
|
|
81
|
+
data = response.json()
|
|
82
|
+
|
|
83
|
+
console.print(Panel(
|
|
84
|
+
f"[green]✓[/green] Memory created: [bold]{data['id']}[/bold]\n"
|
|
85
|
+
f"Type: {data['memory_type']} | Importance: {data['importance']}\n"
|
|
86
|
+
f"Tags: {', '.join(data.get('tags', []))}",
|
|
87
|
+
title="Memory Added",
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@memory.command("get")
|
|
92
|
+
@click.argument("memory_id")
|
|
93
|
+
@click.pass_context
|
|
94
|
+
def memory_get(ctx: click.Context, memory_id: str):
|
|
95
|
+
"""Get a memory by ID"""
|
|
96
|
+
with _get_client() as client:
|
|
97
|
+
response = client.get(f"/api/v1/memory/{memory_id}")
|
|
98
|
+
data = response.json()
|
|
99
|
+
|
|
100
|
+
table = Table(title=f"Memory: {data['id']}")
|
|
101
|
+
table.add_column("Field", style="cyan")
|
|
102
|
+
table.add_column("Value")
|
|
103
|
+
table.add_row("Content", data["content"])
|
|
104
|
+
table.add_row("Type", data["memory_type"])
|
|
105
|
+
table.add_row("Importance", data["importance"])
|
|
106
|
+
table.add_row("Tags", ", ".join(data.get("tags", [])))
|
|
107
|
+
table.add_row("Created", data.get("created_at", "N/A"))
|
|
108
|
+
|
|
109
|
+
console.print(table)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@memory.command("search")
|
|
113
|
+
@click.argument("query")
|
|
114
|
+
@click.option("--limit", "-n", default=5, help="Max results")
|
|
115
|
+
@click.option("--type", "memory_type", default=None, help="Filter by type")
|
|
116
|
+
@click.pass_context
|
|
117
|
+
def memory_search(ctx: click.Context, query: str, limit: int, memory_type: Optional[str]):
|
|
118
|
+
"""Search memories"""
|
|
119
|
+
payload = {
|
|
120
|
+
"query": query,
|
|
121
|
+
"top_k": limit,
|
|
122
|
+
}
|
|
123
|
+
if memory_type:
|
|
124
|
+
payload["memory_type"] = memory_type
|
|
125
|
+
|
|
126
|
+
with _get_client() as client:
|
|
127
|
+
response = client.post("/api/v1/memory/search", json=payload)
|
|
128
|
+
data = response.json()
|
|
129
|
+
|
|
130
|
+
results = data.get("results", [])
|
|
131
|
+
if not results:
|
|
132
|
+
console.print("[yellow]No results found[/yellow]")
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
table = Table(title=f"Search Results for '{query}'")
|
|
136
|
+
table.add_column("#", style="dim")
|
|
137
|
+
table.add_column("Score", style="green")
|
|
138
|
+
table.add_column("Content")
|
|
139
|
+
table.add_column("Type")
|
|
140
|
+
|
|
141
|
+
for i, r in enumerate(results, 1):
|
|
142
|
+
table.add_row(
|
|
143
|
+
str(i),
|
|
144
|
+
f"{r['score']:.2f}",
|
|
145
|
+
r["memory"]["content"][:80] + ("..." if len(r["memory"]["content"]) > 80 else ""),
|
|
146
|
+
r["memory"]["memory_type"],
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
console.print(table)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@memory.command("list")
|
|
153
|
+
@click.option("--type", "memory_type", default=None, help="Filter by type")
|
|
154
|
+
@click.option("--limit", "-n", default=20, help="Max results")
|
|
155
|
+
@click.pass_context
|
|
156
|
+
def memory_list(ctx: click.Context, memory_type: Optional[str], limit: int):
|
|
157
|
+
"""List memories"""
|
|
158
|
+
params = {"limit": limit}
|
|
159
|
+
if memory_type:
|
|
160
|
+
params["memory_type"] = memory_type
|
|
161
|
+
|
|
162
|
+
with _get_client() as client:
|
|
163
|
+
response = client.get("/api/v1/memory", params=params)
|
|
164
|
+
data = response.json()
|
|
165
|
+
|
|
166
|
+
memories = data.get("memories", [])
|
|
167
|
+
if not memories:
|
|
168
|
+
console.print("[yellow]No memories found[/yellow]")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
table = Table(title="Memories")
|
|
172
|
+
table.add_column("ID", style="cyan")
|
|
173
|
+
table.add_column("Content")
|
|
174
|
+
table.add_column("Type")
|
|
175
|
+
table.add_column("Importance")
|
|
176
|
+
|
|
177
|
+
for m in memories:
|
|
178
|
+
content = m["content"][:60] + ("..." if len(m["content"]) > 60 else "")
|
|
179
|
+
table.add_row(m["id"], content, m["memory_type"], m["importance"])
|
|
180
|
+
|
|
181
|
+
console.print(table)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@memory.command("delete")
|
|
185
|
+
@click.argument("memory_id")
|
|
186
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this memory?")
|
|
187
|
+
@click.pass_context
|
|
188
|
+
def memory_delete(ctx: click.Context, memory_id: str):
|
|
189
|
+
"""Delete a memory"""
|
|
190
|
+
with _get_client() as client:
|
|
191
|
+
response = client.delete(f"/api/v1/memory/{memory_id}")
|
|
192
|
+
if response.status_code == 200:
|
|
193
|
+
console.print(f"[green]✓[/green] Memory {memory_id} deleted")
|
|
194
|
+
else:
|
|
195
|
+
console.print(f"[red]✗[/red] Failed to delete memory")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# --- Search Commands ---
|
|
199
|
+
|
|
200
|
+
@cli.group()
|
|
201
|
+
def search():
|
|
202
|
+
"""Search operations"""
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@search.command("web")
|
|
207
|
+
@click.argument("query")
|
|
208
|
+
@click.option("--limit", "-n", default=5, help="Max results")
|
|
209
|
+
@click.pass_context
|
|
210
|
+
def search_web(ctx: click.Context, query: str, limit: int):
|
|
211
|
+
"""Web search"""
|
|
212
|
+
with _get_client() as client:
|
|
213
|
+
response = client.post("/api/v1/search/web", json={"query": query, "max_results": limit})
|
|
214
|
+
data = response.json()
|
|
215
|
+
|
|
216
|
+
results = data.get("results", [])
|
|
217
|
+
if not results:
|
|
218
|
+
console.print("[yellow]No results found[/yellow]")
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
for i, r in enumerate(results, 1):
|
|
222
|
+
console.print(f"\n[bold cyan]{i}. {r['title']}[/bold cyan]")
|
|
223
|
+
console.print(f" [link={r['url']}]{r['url']}[/link]")
|
|
224
|
+
console.print(f" {r['snippet'][:120]}...")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@search.command("internal")
|
|
228
|
+
@click.argument("query")
|
|
229
|
+
@click.option("--limit", "-n", default=10, help="Max results")
|
|
230
|
+
@click.pass_context
|
|
231
|
+
def search_internal(ctx: click.Context, query: str, limit: int):
|
|
232
|
+
"""Internal hybrid search"""
|
|
233
|
+
with _get_client() as client:
|
|
234
|
+
response = client.post("/api/v1/search/internal", json={"query": query, "top_k": limit})
|
|
235
|
+
data = response.json()
|
|
236
|
+
|
|
237
|
+
results = data.get("results", [])
|
|
238
|
+
if not results:
|
|
239
|
+
console.print("[yellow]No results found[/yellow]")
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
table = Table(title=f"Internal Search: '{query}'")
|
|
243
|
+
table.add_column("#", style="dim")
|
|
244
|
+
table.add_column("Score", style="green")
|
|
245
|
+
table.add_column("Title")
|
|
246
|
+
table.add_column("URL")
|
|
247
|
+
|
|
248
|
+
for i, r in enumerate(results, 1):
|
|
249
|
+
table.add_row(str(i), f"{r.get('score', 0):.2f}", r["title"], r["url"])
|
|
250
|
+
|
|
251
|
+
console.print(table)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# --- Crawl Commands ---
|
|
255
|
+
|
|
256
|
+
@cli.group()
|
|
257
|
+
def crawl():
|
|
258
|
+
"""Web crawl operations"""
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@crawl.command("scrape")
|
|
263
|
+
@click.argument("url")
|
|
264
|
+
@click.pass_context
|
|
265
|
+
def crawl_scrape(ctx: click.Context, url: str):
|
|
266
|
+
"""Scrape a single URL"""
|
|
267
|
+
with _get_client() as client:
|
|
268
|
+
response = client.post("/api/v1/crawl/scrape", json={"url": url})
|
|
269
|
+
data = response.json()
|
|
270
|
+
|
|
271
|
+
console.print(Panel(
|
|
272
|
+
f"[bold]{data.get('title', 'N/A')}[/bold]\n\n"
|
|
273
|
+
f"{data['content'][:500]}{'...' if len(data['content']) > 500 else ''}",
|
|
274
|
+
title=f"Scraped: {url}",
|
|
275
|
+
))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@crawl.command("map")
|
|
279
|
+
@click.argument("url")
|
|
280
|
+
@click.option("--limit", "-n", default=50, help="Max pages")
|
|
281
|
+
@click.pass_context
|
|
282
|
+
def crawl_map(ctx: click.Context, url: str, limit: int):
|
|
283
|
+
"""Map a website (get all URLs)"""
|
|
284
|
+
with _get_client() as client:
|
|
285
|
+
response = client.post("/api/v1/crawl/map", json={"url": url, "max_pages": limit})
|
|
286
|
+
data = response.json()
|
|
287
|
+
|
|
288
|
+
urls = data.get("urls", [])
|
|
289
|
+
if not urls:
|
|
290
|
+
console.print("[yellow]No URLs found[/yellow]")
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
console.print(f"[green]Found {len(urls)} URLs:[/green]")
|
|
294
|
+
for u in urls[:limit]:
|
|
295
|
+
console.print(f" {u}")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# --- Knowledge Commands ---
|
|
299
|
+
|
|
300
|
+
@cli.group()
|
|
301
|
+
def knowledge():
|
|
302
|
+
"""Knowledge graph operations"""
|
|
303
|
+
pass
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@knowledge.command("create-entity")
|
|
307
|
+
@click.option("--name", "-n", required=True, help="Entity name")
|
|
308
|
+
@click.option("--type", "entity_type", default="concept", help="Entity type")
|
|
309
|
+
@click.option("--description", "-d", default=None, help="Description")
|
|
310
|
+
@click.pass_context
|
|
311
|
+
def knowledge_create_entity(ctx: click.Context, name: str, entity_type: str, description: Optional[str]):
|
|
312
|
+
"""Create a knowledge graph entity"""
|
|
313
|
+
payload = {
|
|
314
|
+
"name": name,
|
|
315
|
+
"entity_type": entity_type,
|
|
316
|
+
}
|
|
317
|
+
if description:
|
|
318
|
+
payload["description"] = description
|
|
319
|
+
|
|
320
|
+
with _get_client() as client:
|
|
321
|
+
response = client.post("/api/v1/knowledge/entities", json=payload)
|
|
322
|
+
data = response.json()
|
|
323
|
+
|
|
324
|
+
console.print(Panel(
|
|
325
|
+
f"[green]✓[/green] Entity created: [bold]{data['id']}[/bold]\n"
|
|
326
|
+
f"Name: {data['name']} | Type: {data['entity_type']}\n"
|
|
327
|
+
f"Description: {data.get('description', 'N/A')}",
|
|
328
|
+
title="Entity Created",
|
|
329
|
+
))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@knowledge.command("search")
|
|
333
|
+
@click.argument("query")
|
|
334
|
+
@click.option("--type", "entity_type", default=None, help="Filter by type")
|
|
335
|
+
@click.option("--limit", "-n", default=10, help="Max results")
|
|
336
|
+
@click.pass_context
|
|
337
|
+
def knowledge_search(ctx: click.Context, query: str, entity_type: Optional[str], limit: int):
|
|
338
|
+
"""Search entities"""
|
|
339
|
+
payload = {"query": query, "top_k": limit}
|
|
340
|
+
if entity_type:
|
|
341
|
+
payload["entity_type"] = entity_type
|
|
342
|
+
|
|
343
|
+
with _get_client() as client:
|
|
344
|
+
response = client.post("/api/v1/knowledge/search", json=payload)
|
|
345
|
+
data = response.json()
|
|
346
|
+
|
|
347
|
+
entities = data.get("entities", [])
|
|
348
|
+
if not entities:
|
|
349
|
+
console.print("[yellow]No entities found[/yellow]")
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
table = Table(title=f"Entity Search: '{query}'")
|
|
353
|
+
table.add_column("ID", style="cyan")
|
|
354
|
+
table.add_column("Name", style="bold")
|
|
355
|
+
table.add_column("Type")
|
|
356
|
+
table.add_column("Description")
|
|
357
|
+
|
|
358
|
+
for e in entities:
|
|
359
|
+
desc = (e.get("description") or "N/A")[:50]
|
|
360
|
+
table.add_row(e["id"], e["name"], e["entity_type"], desc)
|
|
361
|
+
|
|
362
|
+
console.print(table)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# --- Health Command ---
|
|
366
|
+
|
|
367
|
+
@cli.command()
|
|
368
|
+
def health():
|
|
369
|
+
"""Check API health"""
|
|
370
|
+
with _get_client() as client:
|
|
371
|
+
response = client.get("/api/v1/health")
|
|
372
|
+
data = response.json()
|
|
373
|
+
|
|
374
|
+
status_color = "green" if data.get("status") == "ok" else "red"
|
|
375
|
+
console.print(Panel(
|
|
376
|
+
f"Status: [{status_color}]{data.get('status', 'unknown')}[/{status_color}]\n"
|
|
377
|
+
f"Version: {data.get('version', 'N/A')}\n"
|
|
378
|
+
f"Services: {', '.join(data.get('services', {}).keys()) or 'N/A'}",
|
|
379
|
+
title="API Health",
|
|
380
|
+
))
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
if __name__ == "__main__":
|
|
384
|
+
cli()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ctxos-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CLI for Context OS — Memory, Search, Crawl, Knowledge"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Aman Sagar", email = "aman@amansagar.in" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"context",
|
|
17
|
+
"ai",
|
|
18
|
+
"cli",
|
|
19
|
+
"memory",
|
|
20
|
+
"search",
|
|
21
|
+
"crawl",
|
|
22
|
+
"knowledge-graph",
|
|
23
|
+
"mcp",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 3 - Alpha",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"click>=8.0.0",
|
|
34
|
+
"rich>=13.0.0",
|
|
35
|
+
"httpx>=0.27.0",
|
|
36
|
+
"pydantic>=2.0.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
contextos = "context_cli.main:cli"
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest>=8.0.0",
|
|
45
|
+
"ruff>=0.4.0",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[project.urls]
|
|
49
|
+
Homepage = "https://github.com/AmanSagar0607/Context-OS"
|
|
50
|
+
Repository = "https://github.com/AmanSagar0607/Context-OS"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.wheel]
|
|
53
|
+
packages = ["context_cli"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
target-version = "py310"
|
|
57
|
+
line-length = 100
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context OS — CLI Tests
|
|
3
|
+
|
|
4
|
+
Tests for CLI commands.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from click.testing import CliRunner
|
|
9
|
+
from unittest.mock import patch, MagicMock
|
|
10
|
+
|
|
11
|
+
from context_cli.main import cli
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def runner():
|
|
16
|
+
return CliRunner()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestCLI:
|
|
20
|
+
def test_version(self, runner):
|
|
21
|
+
result = runner.invoke(cli, ["--version"])
|
|
22
|
+
assert result.exit_code == 0
|
|
23
|
+
assert "0.1.0" in result.output
|
|
24
|
+
|
|
25
|
+
def test_help(self, runner):
|
|
26
|
+
result = runner.invoke(cli, ["--help"])
|
|
27
|
+
assert result.exit_code == 0
|
|
28
|
+
assert "Context OS" in result.output
|
|
29
|
+
|
|
30
|
+
def test_memory_help(self, runner):
|
|
31
|
+
result = runner.invoke(cli, ["memory", "--help"])
|
|
32
|
+
assert result.exit_code == 0
|
|
33
|
+
assert "Memory operations" in result.output
|
|
34
|
+
|
|
35
|
+
def test_search_help(self, runner):
|
|
36
|
+
result = runner.invoke(cli, ["search", "--help"])
|
|
37
|
+
assert result.exit_code == 0
|
|
38
|
+
assert "Search operations" in result.output
|
|
39
|
+
|
|
40
|
+
def test_crawl_help(self, runner):
|
|
41
|
+
result = runner.invoke(cli, ["crawl", "--help"])
|
|
42
|
+
assert result.exit_code == 0
|
|
43
|
+
assert "Web crawl operations" in result.output
|
|
44
|
+
|
|
45
|
+
def test_knowledge_help(self, runner):
|
|
46
|
+
result = runner.invoke(cli, ["knowledge", "--help"])
|
|
47
|
+
assert result.exit_code == 0
|
|
48
|
+
assert "Knowledge graph" in result.output
|
|
49
|
+
|
|
50
|
+
@patch("context_cli.main._get_client")
|
|
51
|
+
def test_health(self, mock_get_client, runner):
|
|
52
|
+
mock_client = MagicMock()
|
|
53
|
+
mock_client.__enter__ = MagicMock(return_value=mock_client)
|
|
54
|
+
mock_client.__exit__ = MagicMock(return_value=False)
|
|
55
|
+
mock_response = MagicMock()
|
|
56
|
+
mock_response.json.return_value = {
|
|
57
|
+
"status": "ok",
|
|
58
|
+
"version": "0.1.0",
|
|
59
|
+
"services": {"postgres": "connected"},
|
|
60
|
+
}
|
|
61
|
+
mock_client.get.return_value = mock_response
|
|
62
|
+
mock_get_client.return_value = mock_client
|
|
63
|
+
|
|
64
|
+
result = runner.invoke(cli, ["health"])
|
|
65
|
+
assert result.exit_code == 0
|
|
66
|
+
assert "ok" in result.output.lower() or "health" in result.output.lower()
|
|
67
|
+
|
|
68
|
+
@patch("context_cli.main._get_client")
|
|
69
|
+
def test_memory_add_help(self, mock_get_client, runner):
|
|
70
|
+
result = runner.invoke(cli, ["memory", "add", "--help"])
|
|
71
|
+
assert result.exit_code == 0
|
|
72
|
+
assert "Memory content" in result.output
|
|
73
|
+
|
|
74
|
+
@patch("context_cli.main._get_client")
|
|
75
|
+
def test_memory_search_help(self, mock_get_client, runner):
|
|
76
|
+
result = runner.invoke(cli, ["memory", "search", "--help"])
|
|
77
|
+
assert result.exit_code == 0
|
|
78
|
+
assert "query" in result.output.lower()
|