cortexdb-cli 0.2.0__py3-none-any.whl → 0.4.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.
cortexdb_cli/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """CortexDB CLI — command-line interface for the v1 memory API."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.4.0"
cortexdb_cli/client.py CHANGED
@@ -1,92 +1,92 @@
1
- """HTTP client for the CortexDB v1 API.
2
-
3
- Thin wrapper around httpx that reads credentials + endpoint from the CLI
4
- context and stamps every request with both `Authorization: Bearer …` and
5
- `X-Cortex-Actor: …`. The actor header is mandatory on v1 — missing it
6
- returns 401 ACTOR_MISMATCH no matter how valid the token is.
7
-
8
- We deliberately don't depend on the cortexdbai SDK: keeping the CLI a
9
- straight HTTP client makes the install fast and lets us pin httpx
10
- independently from the SDK's release cadence.
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- from contextlib import contextmanager
16
- from typing import Any, Generator
17
-
18
- import httpx
19
-
20
- from cortexdb_cli import __version__
21
-
22
- DEFAULT_ENDPOINT = "https://api-v1.cortexdb.ai"
23
-
24
-
25
- class CortexClient:
26
- """Synchronous HTTP client for the CortexDB v1 API."""
27
-
28
- def __init__(
29
- self,
30
- endpoint: str,
31
- api_key: str,
32
- actor: str = "",
33
- timeout: float = 30.0,
34
- ) -> None:
35
- self.endpoint = endpoint.rstrip("/")
36
- self.api_key = api_key
37
- self.actor = actor
38
- headers: dict[str, str] = {
39
- "Content-Type": "application/json",
40
- "User-Agent": f"cortexdb-cli/{__version__}",
41
- }
42
- if api_key:
43
- headers["Authorization"] = f"Bearer {api_key}"
44
- if actor:
45
- headers["X-Cortex-Actor"] = actor
46
- self._http = httpx.Client(
47
- base_url=self.endpoint,
48
- headers=headers,
49
- timeout=timeout,
50
- )
51
-
52
- def close(self) -> None:
53
- self._http.close()
54
-
55
- def get(self, path: str, params: dict[str, Any] | None = None) -> httpx.Response:
56
- return self._http.get(path, params=params)
57
-
58
- def post(
59
- self,
60
- path: str,
61
- json: dict[str, Any] | None = None,
62
- params: dict[str, Any] | None = None,
63
- ) -> httpx.Response:
64
- return self._http.post(path, json=json, params=params)
65
-
66
- def delete(self, path: str, params: dict[str, Any] | None = None) -> httpx.Response:
67
- return self._http.delete(path, params=params)
68
-
69
-
70
- def signup(endpoint: str = DEFAULT_ENDPOINT, timeout: float = 30.0) -> dict[str, Any]:
71
- """Call POST /v1/auth/signup and return {token, user_id, scope, expires_at, tier}.
72
-
73
- The endpoint is public — no auth required. Free tier; 7-day TTL.
74
- """
75
- with httpx.Client(timeout=timeout) as http:
76
- resp = http.post(f"{endpoint.rstrip('/')}/v1/auth/signup", json={})
77
- resp.raise_for_status()
78
- return resp.json()
79
-
80
-
81
- @contextmanager
82
- def get_client(ctx: dict[str, Any]) -> Generator[CortexClient, None, None]:
83
- """Create a CortexClient from CLI context."""
84
- client = CortexClient(
85
- endpoint=ctx.get("endpoint", DEFAULT_ENDPOINT),
86
- api_key=ctx.get("api_key", ""),
87
- actor=ctx.get("actor", ""),
88
- )
89
- try:
90
- yield client
91
- finally:
92
- client.close()
1
+ """HTTP client for the CortexDB v1 API.
2
+
3
+ Thin wrapper around httpx that reads credentials + endpoint from the CLI
4
+ context and stamps every request with both `Authorization: Bearer …` and
5
+ `X-Cortex-Actor: …`. The actor header is mandatory on v1 — missing it
6
+ returns 401 ACTOR_MISMATCH no matter how valid the token is.
7
+
8
+ We deliberately don't depend on the cortexdbai SDK: keeping the CLI a
9
+ straight HTTP client makes the install fast and lets us pin httpx
10
+ independently from the SDK's release cadence.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from contextlib import contextmanager
16
+ from typing import Any, Generator
17
+
18
+ import httpx
19
+
20
+ from cortexdb_cli import __version__
21
+
22
+ DEFAULT_ENDPOINT = "https://api-v1.cortexdb.ai"
23
+
24
+
25
+ class CortexClient:
26
+ """Synchronous HTTP client for the CortexDB v1 API."""
27
+
28
+ def __init__(
29
+ self,
30
+ endpoint: str,
31
+ api_key: str,
32
+ actor: str = "",
33
+ timeout: float = 30.0,
34
+ ) -> None:
35
+ self.endpoint = endpoint.rstrip("/")
36
+ self.api_key = api_key
37
+ self.actor = actor
38
+ headers: dict[str, str] = {
39
+ "Content-Type": "application/json",
40
+ "User-Agent": f"cortexdb-cli/{__version__}",
41
+ }
42
+ if api_key:
43
+ headers["Authorization"] = f"Bearer {api_key}"
44
+ if actor:
45
+ headers["X-Cortex-Actor"] = actor
46
+ self._http = httpx.Client(
47
+ base_url=self.endpoint,
48
+ headers=headers,
49
+ timeout=timeout,
50
+ )
51
+
52
+ def close(self) -> None:
53
+ self._http.close()
54
+
55
+ def get(self, path: str, params: dict[str, Any] | None = None) -> httpx.Response:
56
+ return self._http.get(path, params=params)
57
+
58
+ def post(
59
+ self,
60
+ path: str,
61
+ json: dict[str, Any] | None = None,
62
+ params: dict[str, Any] | None = None,
63
+ ) -> httpx.Response:
64
+ return self._http.post(path, json=json, params=params)
65
+
66
+ def delete(self, path: str, params: dict[str, Any] | None = None) -> httpx.Response:
67
+ return self._http.delete(path, params=params)
68
+
69
+
70
+ def signup(endpoint: str = DEFAULT_ENDPOINT, timeout: float = 30.0) -> dict[str, Any]:
71
+ """Call POST /v1/auth/signup and return {token, user_id, scope, expires_at, tier}.
72
+
73
+ The endpoint is public — no auth required. Free tier; 7-day TTL.
74
+ """
75
+ with httpx.Client(timeout=timeout) as http:
76
+ resp = http.post(f"{endpoint.rstrip('/')}/v1/auth/signup", json={})
77
+ resp.raise_for_status()
78
+ return resp.json()
79
+
80
+
81
+ @contextmanager
82
+ def get_client(ctx: dict[str, Any]) -> Generator[CortexClient, None, None]:
83
+ """Create a CortexClient from CLI context."""
84
+ client = CortexClient(
85
+ endpoint=ctx.get("endpoint", DEFAULT_ENDPOINT),
86
+ api_key=ctx.get("api_key", ""),
87
+ actor=ctx.get("actor", ""),
88
+ )
89
+ try:
90
+ yield client
91
+ finally:
92
+ client.close()
@@ -1,83 +1,83 @@
1
- """cortexdb admin — health, usage, layer stats."""
2
-
3
- from __future__ import annotations
4
-
5
- import click
6
-
7
- from cortexdb_cli.client import get_client
8
- from cortexdb_cli.errors import handle_errors
9
- from cortexdb_cli.output import (
10
- is_json_mode,
11
- print_health,
12
- print_ontology,
13
- print_usage,
14
- spinner,
15
- )
16
-
17
-
18
- @click.group("admin")
19
- def admin_group() -> None:
20
- """Diagnostics — reachability, identity, rate-limit, layer status."""
21
-
22
-
23
- @admin_group.command("health")
24
- @click.pass_context
25
- @handle_errors
26
- def admin_health(ctx: click.Context) -> None:
27
- """Auth + reachability check (GET /v1/auth/whoami)."""
28
- cfg = ctx.obj
29
- with get_client(cfg) as client:
30
- resp = client.get("/v1/auth/whoami")
31
- resp.raise_for_status()
32
- body = resp.json()
33
- # Normalise into the shape print_health expects.
34
- body.setdefault("status", "ok")
35
- print_health(body, cfg)
36
-
37
-
38
- @admin_group.command("usage")
39
- @click.pass_context
40
- @handle_errors
41
- def admin_usage(ctx: click.Context) -> None:
42
- """Tier, capabilities, rate-limit headroom, token expiry."""
43
- cfg = ctx.obj
44
- with get_client(cfg) as client:
45
- if not is_json_mode(cfg):
46
- with spinner("Loading usage..."):
47
- resp = client.get("/v1/auth/whoami")
48
- else:
49
- resp = client.get("/v1/auth/whoami")
50
- resp.raise_for_status()
51
- body = resp.json()
52
- # Fold rate-limit + expiry response headers into the body so the
53
- # output formatter can render them in one block.
54
- for k in (
55
- "X-RateLimit-Limit",
56
- "X-RateLimit-Reset",
57
- "X-RateLimit-Remaining",
58
- "X-Cortex-Token-Expires-In",
59
- "X-Cortex-Token-Expires-At",
60
- ):
61
- if k in resp.headers:
62
- body[k] = resp.headers[k]
63
- print_usage(body, cfg)
64
-
65
-
66
- @admin_group.command("layers")
67
- @click.pass_context
68
- @handle_errors
69
- def admin_layers(ctx: click.Context) -> None:
70
- """Per-layer counts and synthesis lag (GET /v1/admin/layers/stats).
71
-
72
- Stability: experimental — the endpoint is gated by the
73
- `admin.layers.stats` capability and may not be present on the free tier.
74
- """
75
- cfg = ctx.obj
76
- with get_client(cfg) as client:
77
- if not is_json_mode(cfg):
78
- with spinner("Loading layer stats..."):
79
- resp = client.get("/v1/admin/layers/stats")
80
- else:
81
- resp = client.get("/v1/admin/layers/stats")
82
- resp.raise_for_status()
83
- print_ontology(resp.json(), cfg)
1
+ """cortexdb admin — health, usage, layer stats."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from cortexdb_cli.client import get_client
8
+ from cortexdb_cli.errors import handle_errors
9
+ from cortexdb_cli.output import (
10
+ is_json_mode,
11
+ print_health,
12
+ print_ontology,
13
+ print_usage,
14
+ spinner,
15
+ )
16
+
17
+
18
+ @click.group("admin")
19
+ def admin_group() -> None:
20
+ """Diagnostics — reachability, identity, rate-limit, layer status."""
21
+
22
+
23
+ @admin_group.command("health")
24
+ @click.pass_context
25
+ @handle_errors
26
+ def admin_health(ctx: click.Context) -> None:
27
+ """Auth + reachability check (GET /v1/auth/whoami)."""
28
+ cfg = ctx.obj
29
+ with get_client(cfg) as client:
30
+ resp = client.get("/v1/auth/whoami")
31
+ resp.raise_for_status()
32
+ body = resp.json()
33
+ # Normalise into the shape print_health expects.
34
+ body.setdefault("status", "ok")
35
+ print_health(body, cfg)
36
+
37
+
38
+ @admin_group.command("usage")
39
+ @click.pass_context
40
+ @handle_errors
41
+ def admin_usage(ctx: click.Context) -> None:
42
+ """Tier, capabilities, rate-limit headroom, token expiry."""
43
+ cfg = ctx.obj
44
+ with get_client(cfg) as client:
45
+ if not is_json_mode(cfg):
46
+ with spinner("Loading usage..."):
47
+ resp = client.get("/v1/auth/whoami")
48
+ else:
49
+ resp = client.get("/v1/auth/whoami")
50
+ resp.raise_for_status()
51
+ body = resp.json()
52
+ # Fold rate-limit + expiry response headers into the body so the
53
+ # output formatter can render them in one block.
54
+ for k in (
55
+ "X-RateLimit-Limit",
56
+ "X-RateLimit-Reset",
57
+ "X-RateLimit-Remaining",
58
+ "X-Cortex-Token-Expires-In",
59
+ "X-Cortex-Token-Expires-At",
60
+ ):
61
+ if k in resp.headers:
62
+ body[k] = resp.headers[k]
63
+ print_usage(body, cfg)
64
+
65
+
66
+ @admin_group.command("layers")
67
+ @click.pass_context
68
+ @handle_errors
69
+ def admin_layers(ctx: click.Context) -> None:
70
+ """Per-layer counts and synthesis lag (GET /v1/admin/layers/stats).
71
+
72
+ Stability: experimental — the endpoint is gated by the
73
+ `admin.layers.stats` capability and may not be present on the free tier.
74
+ """
75
+ cfg = ctx.obj
76
+ with get_client(cfg) as client:
77
+ if not is_json_mode(cfg):
78
+ with spinner("Loading layer stats..."):
79
+ resp = client.get("/v1/admin/layers/stats")
80
+ else:
81
+ resp = client.get("/v1/admin/layers/stats")
82
+ resp.raise_for_status()
83
+ print_ontology(resp.json(), cfg)
@@ -1,98 +1,105 @@
1
- """cortexdb answer — POST /v1/answer (recall + LLM answer)."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import Any
6
-
7
- import click
8
-
9
- from cortexdb_cli.client import get_client
10
- from cortexdb_cli.errors import handle_errors
11
- from cortexdb_cli.output import console, is_json_mode, print_json, spinner
12
-
13
-
14
- @click.command("answer")
15
- @click.argument("question")
16
- @click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
17
- @click.option(
18
- "--view",
19
- type=click.Choice(["raw", "granular", "structured", "holistic", "descend"]),
20
- default="holistic",
21
- show_default=True,
22
- )
23
- @click.option("--temporal", help='Natural-language time window, e.g. "last 30 days"')
24
- @click.option("--answer-model", help="Override the answer model (paid tier only).")
25
- @click.option(
26
- "--diagnostics",
27
- type=click.Choice(["none", "summary", "full"]),
28
- default="none",
29
- show_default=True,
30
- )
31
- @click.pass_context
32
- @handle_errors
33
- def answer_cmd(
34
- ctx: click.Context,
35
- question: str,
36
- scope: str | None,
37
- view: str,
38
- temporal: str | None,
39
- answer_model: str | None,
40
- diagnostics: str,
41
- ) -> None:
42
- """Ask a question — server recalls context and an LLM answers it.
43
-
44
- Counts against the tenant's LLM quota. Requires the `llm.invoke`
45
- capability (granted by default on every tier including Free).
46
-
47
- cortexdb answer "Did Acme renew?" --temporal "last 30 days"
48
- """
49
- cfg = ctx.obj
50
- target_scope = scope or cfg.get("scope") or cfg.get("actor")
51
- if not target_scope:
52
- raise click.UsageError(
53
- "No scope configured. Run `cortexdb init` first, or pass --scope explicitly."
54
- )
55
-
56
- body: dict[str, Any] = {
57
- "scope": target_scope,
58
- "question": question,
59
- "view": view,
60
- "diagnostics": diagnostics,
61
- }
62
- if temporal:
63
- body["temporal"] = {"natural": temporal}
64
- if answer_model:
65
- body["answer_model"] = answer_model
66
-
67
- with get_client(cfg) as client:
68
- if not is_json_mode(cfg):
69
- with spinner("Answering..."):
70
- resp = client.post("/v1/answer", json=body)
71
- else:
72
- resp = client.post("/v1/answer", json=body)
73
- resp.raise_for_status()
74
- data = resp.json()
75
-
76
- if is_json_mode(cfg):
77
- print_json(data)
78
- return
79
-
80
- from rich.markup import escape
81
- from rich.panel import Panel
82
-
83
- text = data.get("answer") or data.get("text") or "(no answer returned)"
84
- cites = data.get("citations") or []
85
-
86
- console.print(
87
- Panel(
88
- escape(str(text)),
89
- title="[header]Answer[/header]",
90
- border_style="cyan",
91
- padding=(1, 2),
92
- )
93
- )
94
- if cites:
95
- console.print(f"\n[dim]{len(cites)} citation(s):[/dim]")
96
- for c in cites[:10]:
97
- eid = c.get("event_id") or c.get("id") or ""
98
- console.print(f" [dim]· {escape(eid)}[/dim]")
1
+ """cortexdb answer — POST /v1/answer (recall + LLM answer)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import click
8
+
9
+ from cortexdb_cli.client import get_client
10
+ from cortexdb_cli.errors import handle_errors
11
+ from cortexdb_cli.output import console, is_json_mode, print_json, spinner
12
+
13
+
14
+ @click.command("answer")
15
+ @click.argument("question")
16
+ @click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
17
+ @click.option(
18
+ "--view",
19
+ type=click.Choice(["raw", "granular", "structured", "holistic", "descend"]),
20
+ default=None,
21
+ help="Pack shape used for recall. Defaults to your configured view "
22
+ "(CORTEXDB_VIEW env or `cortexdb config set view ...`), else 'holistic'. "
23
+ "Use 'descend' to search the scope + all child scopes.",
24
+ )
25
+ @click.option("--temporal", help='Natural-language time window, e.g. "last 30 days"')
26
+ @click.option("--answer-model", help="Override the answer model (paid tier only).")
27
+ @click.option(
28
+ "--diagnostics",
29
+ type=click.Choice(["none", "summary", "full"]),
30
+ default="none",
31
+ show_default=True,
32
+ )
33
+ @click.pass_context
34
+ @handle_errors
35
+ def answer_cmd(
36
+ ctx: click.Context,
37
+ question: str,
38
+ scope: str | None,
39
+ view: str,
40
+ temporal: str | None,
41
+ answer_model: str | None,
42
+ diagnostics: str,
43
+ ) -> None:
44
+ """Ask a question server recalls context and an LLM answers it.
45
+
46
+ Counts against the tenant's LLM quota. Requires the `llm.invoke`
47
+ capability (granted by default on every tier including Free).
48
+
49
+ cortexdb answer "Did Acme renew?" --temporal "last 30 days"
50
+ """
51
+ cfg = ctx.obj
52
+ view = view or cfg.get("view") or "holistic"
53
+ if view not in ("raw", "granular", "structured", "holistic", "descend"):
54
+ raise click.UsageError(
55
+ f"Invalid view {view!r}. Choose: raw, granular, structured, holistic, descend."
56
+ )
57
+ target_scope = scope or cfg.get("scope") or cfg.get("actor")
58
+ if not target_scope:
59
+ raise click.UsageError(
60
+ "No scope configured. Run `cortexdb init` first, or pass --scope explicitly."
61
+ )
62
+
63
+ body: dict[str, Any] = {
64
+ "scope": target_scope,
65
+ "question": question,
66
+ "view": view,
67
+ "diagnostics": diagnostics,
68
+ }
69
+ if temporal:
70
+ body["temporal"] = {"natural": temporal}
71
+ if answer_model:
72
+ body["answer_model"] = answer_model
73
+
74
+ with get_client(cfg) as client:
75
+ if not is_json_mode(cfg):
76
+ with spinner("Answering..."):
77
+ resp = client.post("/v1/answer", json=body)
78
+ else:
79
+ resp = client.post("/v1/answer", json=body)
80
+ resp.raise_for_status()
81
+ data = resp.json()
82
+
83
+ if is_json_mode(cfg):
84
+ print_json(data)
85
+ return
86
+
87
+ from rich.markup import escape
88
+ from rich.panel import Panel
89
+
90
+ text = data.get("answer") or data.get("text") or "(no answer returned)"
91
+ cites = data.get("citations") or []
92
+
93
+ console.print(
94
+ Panel(
95
+ escape(str(text)),
96
+ title="[header]Answer[/header]",
97
+ border_style="cyan",
98
+ padding=(1, 2),
99
+ )
100
+ )
101
+ if cites:
102
+ console.print(f"\n[dim]{len(cites)} citation(s):[/dim]")
103
+ for c in cites[:10]:
104
+ eid = c.get("event_id") or c.get("id") or ""
105
+ console.print(f" [dim]· {escape(eid)}[/dim]")