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.
@@ -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
@@ -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,7 @@
1
+ """
2
+ Context OS — CLI
3
+
4
+ Command-line interface for Context OS operations.
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -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()