cortexdb-cli 0.2.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.
Files changed (31) hide show
  1. cortexdb_cli-0.2.0/.gitignore +71 -0
  2. cortexdb_cli-0.2.0/PKG-INFO +122 -0
  3. cortexdb_cli-0.2.0/README.md +90 -0
  4. cortexdb_cli-0.2.0/cortexdb_cli/__init__.py +3 -0
  5. cortexdb_cli-0.2.0/cortexdb_cli/client.py +92 -0
  6. cortexdb_cli-0.2.0/cortexdb_cli/commands/__init__.py +1 -0
  7. cortexdb_cli-0.2.0/cortexdb_cli/commands/admin.py +83 -0
  8. cortexdb_cli-0.2.0/cortexdb_cli/commands/answer.py +98 -0
  9. cortexdb_cli-0.2.0/cortexdb_cli/commands/auth.py +128 -0
  10. cortexdb_cli-0.2.0/cortexdb_cli/commands/beliefs.py +121 -0
  11. cortexdb_cli-0.2.0/cortexdb_cli/commands/config_cmd.py +50 -0
  12. cortexdb_cli-0.2.0/cortexdb_cli/commands/entities.py +174 -0
  13. cortexdb_cli-0.2.0/cortexdb_cli/commands/episodes.py +131 -0
  14. cortexdb_cli-0.2.0/cortexdb_cli/commands/experience.py +238 -0
  15. cortexdb_cli-0.2.0/cortexdb_cli/commands/export_cmd.py +65 -0
  16. cortexdb_cli-0.2.0/cortexdb_cli/commands/facts.py +171 -0
  17. cortexdb_cli-0.2.0/cortexdb_cli/commands/forget.py +86 -0
  18. cortexdb_cli-0.2.0/cortexdb_cli/commands/import_cmd.py +198 -0
  19. cortexdb_cli-0.2.0/cortexdb_cli/commands/init.py +95 -0
  20. cortexdb_cli-0.2.0/cortexdb_cli/commands/interactive.py +254 -0
  21. cortexdb_cli-0.2.0/cortexdb_cli/commands/policy.py +99 -0
  22. cortexdb_cli-0.2.0/cortexdb_cli/commands/recall.py +93 -0
  23. cortexdb_cli-0.2.0/cortexdb_cli/commands/scopes.py +98 -0
  24. cortexdb_cli-0.2.0/cortexdb_cli/commands/search.py +64 -0
  25. cortexdb_cli-0.2.0/cortexdb_cli/commands/understanding.py +141 -0
  26. cortexdb_cli-0.2.0/cortexdb_cli/config.py +113 -0
  27. cortexdb_cli-0.2.0/cortexdb_cli/errors.py +96 -0
  28. cortexdb_cli-0.2.0/cortexdb_cli/main.py +140 -0
  29. cortexdb_cli-0.2.0/cortexdb_cli/output.py +399 -0
  30. cortexdb_cli-0.2.0/cortexdb_cli/state.py +53 -0
  31. cortexdb_cli-0.2.0/pyproject.toml +56 -0
@@ -0,0 +1,71 @@
1
+ # Rust
2
+ /target
3
+ **/*.rs.bk
4
+
5
+ # Environment / secrets
6
+ .env
7
+ .env.local
8
+ .env*.local
9
+ *.pem
10
+ *.key
11
+ .npmrc
12
+
13
+ # SQLite database
14
+ *.sqlite
15
+ *.sqlite-wal
16
+ *.sqlite-shm
17
+
18
+ # OS
19
+ .DS_Store
20
+ Thumbs.db
21
+ desktop.ini
22
+
23
+ # IDE
24
+ .idea/
25
+ .vscode/
26
+ *.swp
27
+ *.swo
28
+
29
+ # Data directories
30
+ cortexdb_data*/
31
+ /data/
32
+ # Per-bench tenant stores (RocksDB + Tantivy + HNSW state; regeneratable per run)
33
+ /data_*/
34
+ # Experimental per-branch stores (not tracked on this branch but left gitignored
35
+ # so checkout from other branches doesn't surface them in git status)
36
+ /event_memory_store/
37
+ /llm_cache/
38
+
39
+ # Benchmark inputs and per-run outputs (kept local, regenerated each run)
40
+ benchmarks/longmemeval/data/
41
+ benchmarks/longmemeval/server_results/
42
+ benchmarks/longmemeval/fast_results/
43
+ benchmarks/longmemeval/micro_results/
44
+ benchmarks/longmemeval/server_logs/
45
+ benchmarks/longmemeval/*.log
46
+ benchmarks/locomo/locomo_results*.json
47
+ benchmarks/locomo/server_results/
48
+ benchmarks/locomo/*.log
49
+ /answer_out.json
50
+
51
+ # Local Claude Code state
52
+ .claude/
53
+ .tmp/
54
+
55
+ # Python
56
+ __pycache__/
57
+ *.pyc
58
+ .venv/
59
+ venv/
60
+
61
+ # Node
62
+ node_modules/
63
+ dist/
64
+ .next/
65
+
66
+ # Egg info
67
+ *.egg-info/
68
+
69
+ # Scratch/debug text files at root
70
+ /*.txt
71
+ /*.log
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: cortexdb-cli
3
+ Version: 0.2.0
4
+ Summary: Command-line interface for CortexDB — the long-term memory layer for AI.
5
+ Project-URL: Homepage, https://cortexdb.ai
6
+ Project-URL: Documentation, https://cortexdb.ai/docs
7
+ Project-URL: Repository, https://github.com/cortexdb/cortexdb
8
+ Project-URL: Issues, https://github.com/cortexdb/cortexdb/issues
9
+ Author-email: CortexDB Team <team@cortexdb.ai>
10
+ License-Expression: Apache-2.0
11
+ Keywords: agents,ai,cli,cortexdb,llm,memory
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Database
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: click>=8.1
25
+ Requires-Dist: httpx>=0.27
26
+ Requires-Dist: prompt-toolkit>=3.0
27
+ Requires-Dist: rich-click>=1.7
28
+ Requires-Dist: rich>=13.0
29
+ Requires-Dist: tomli-w>=1.0
30
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # cortexdb-cli
34
+
35
+ Command-line interface for [CortexDB](https://cortexdb.ai) — the long-term memory layer for AI systems.
36
+
37
+ Wraps the v1 HTTP API in a small, fast Click + Rich CLI. Anonymous signup
38
+ in one command, no email or card; works against the public SaaS or any
39
+ self-hosted CortexDB deployment.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install cortexdb-cli
45
+ ```
46
+
47
+ Requires Python 3.10+.
48
+
49
+ ## Quickstart
50
+
51
+ ```bash
52
+ # 1. One-time setup — anonymous signup, writes ~/.cortexdb/state.json
53
+ cortexdb init
54
+
55
+ # 2. Store a memory
56
+ cortexdb remember "Q3 revenue: $2.4M. We're on track for $10M ARR by year-end."
57
+
58
+ # 3. Recall it
59
+ cortexdb recall "Q3 revenue?"
60
+
61
+ # 4. Or just type — drops into a REPL
62
+ cortexdb
63
+ ```
64
+
65
+ The signup token has a 7-day TTL on the free tier. Re-run `cortexdb init`
66
+ to refresh, or pass `--api-key` + `--actor` to use a token from your IdP.
67
+
68
+ ## Commands
69
+
70
+ ```
71
+ cortexdb init anonymous signup; persist token + actor
72
+ cortexdb remember "..." store a memory (POST /v1/experience)
73
+ cortexdb recall "..." recall a stratified context pack
74
+ cortexdb search "..." granular event-level search
75
+ cortexdb forget --memory-id ... delete with audit
76
+ cortexdb episodes {list,get,delete} episode CRUD
77
+ cortexdb entities {list,get,link} entity rollups over the Facts layer
78
+ cortexdb admin {health,usage,layers} diagnostics
79
+ cortexdb import data.json bulk-load (JSON / JSONL / CSV / TXT)
80
+ cortexdb export -o backup.json export memories
81
+ cortexdb config {show,set} view / edit ~/.cortexdb/config.toml
82
+ cortexdb interactive REPL
83
+ ```
84
+
85
+ ## Configuration
86
+
87
+ Three layers, in precedence order:
88
+
89
+ 1. **CLI flags** — `--api-key`, `--endpoint`, `--actor`, `--scope`, `--profile`
90
+ 2. **Environment** — `CORTEXDB_API_KEY`, `CORTEXDB_URL`, `CORTEXDB_ACTOR`, `CORTEXDB_SCOPE`
91
+ 3. **Persisted state**:
92
+ - `~/.cortexdb/config.toml` — endpoint, profile names (human-editable)
93
+ - `~/.cortexdb/state.json` — token, actor, scope, expiry (managed by `init`, 0600 on POSIX)
94
+
95
+ The state.json format matches the cortexdb-mcp 0.3.0 server's, so you can
96
+ copy state across to use both tools from the same anonymous identity.
97
+
98
+ ## Pipe-friendly
99
+
100
+ Auto-detects when stdout isn't a TTY and emits JSON:
101
+
102
+ ```bash
103
+ cortexdb recall "Q3 revenue" | jq .context_block
104
+ cortexdb entities list | jq '.[].subject'
105
+ echo "remember this" | cortexdb remember -
106
+ ```
107
+
108
+ Force JSON mode any time with `--json`.
109
+
110
+ ## Auth notes
111
+
112
+ - The v1 API requires both `Authorization: Bearer <PASETO>` and `X-Cortex-Actor`.
113
+ The CLI stamps both on every request — you never need to pass them by hand.
114
+ - 401s show the specific error code (`TOKEN_EXPIRED`, `actor_mismatch`, …)
115
+ and the remediation: usually `cortexdb init`.
116
+ - 403s cite the missing capability and the policy tier that denied. If a
117
+ recall returns `diagnostics.read denied`, pass `--diagnostics none` —
118
+ free-tier tokens don't carry the `diagnostics.read` capability.
119
+
120
+ ## License
121
+
122
+ Apache-2.0
@@ -0,0 +1,90 @@
1
+ # cortexdb-cli
2
+
3
+ Command-line interface for [CortexDB](https://cortexdb.ai) — the long-term memory layer for AI systems.
4
+
5
+ Wraps the v1 HTTP API in a small, fast Click + Rich CLI. Anonymous signup
6
+ in one command, no email or card; works against the public SaaS or any
7
+ self-hosted CortexDB deployment.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install cortexdb-cli
13
+ ```
14
+
15
+ Requires Python 3.10+.
16
+
17
+ ## Quickstart
18
+
19
+ ```bash
20
+ # 1. One-time setup — anonymous signup, writes ~/.cortexdb/state.json
21
+ cortexdb init
22
+
23
+ # 2. Store a memory
24
+ cortexdb remember "Q3 revenue: $2.4M. We're on track for $10M ARR by year-end."
25
+
26
+ # 3. Recall it
27
+ cortexdb recall "Q3 revenue?"
28
+
29
+ # 4. Or just type — drops into a REPL
30
+ cortexdb
31
+ ```
32
+
33
+ The signup token has a 7-day TTL on the free tier. Re-run `cortexdb init`
34
+ to refresh, or pass `--api-key` + `--actor` to use a token from your IdP.
35
+
36
+ ## Commands
37
+
38
+ ```
39
+ cortexdb init anonymous signup; persist token + actor
40
+ cortexdb remember "..." store a memory (POST /v1/experience)
41
+ cortexdb recall "..." recall a stratified context pack
42
+ cortexdb search "..." granular event-level search
43
+ cortexdb forget --memory-id ... delete with audit
44
+ cortexdb episodes {list,get,delete} episode CRUD
45
+ cortexdb entities {list,get,link} entity rollups over the Facts layer
46
+ cortexdb admin {health,usage,layers} diagnostics
47
+ cortexdb import data.json bulk-load (JSON / JSONL / CSV / TXT)
48
+ cortexdb export -o backup.json export memories
49
+ cortexdb config {show,set} view / edit ~/.cortexdb/config.toml
50
+ cortexdb interactive REPL
51
+ ```
52
+
53
+ ## Configuration
54
+
55
+ Three layers, in precedence order:
56
+
57
+ 1. **CLI flags** — `--api-key`, `--endpoint`, `--actor`, `--scope`, `--profile`
58
+ 2. **Environment** — `CORTEXDB_API_KEY`, `CORTEXDB_URL`, `CORTEXDB_ACTOR`, `CORTEXDB_SCOPE`
59
+ 3. **Persisted state**:
60
+ - `~/.cortexdb/config.toml` — endpoint, profile names (human-editable)
61
+ - `~/.cortexdb/state.json` — token, actor, scope, expiry (managed by `init`, 0600 on POSIX)
62
+
63
+ The state.json format matches the cortexdb-mcp 0.3.0 server's, so you can
64
+ copy state across to use both tools from the same anonymous identity.
65
+
66
+ ## Pipe-friendly
67
+
68
+ Auto-detects when stdout isn't a TTY and emits JSON:
69
+
70
+ ```bash
71
+ cortexdb recall "Q3 revenue" | jq .context_block
72
+ cortexdb entities list | jq '.[].subject'
73
+ echo "remember this" | cortexdb remember -
74
+ ```
75
+
76
+ Force JSON mode any time with `--json`.
77
+
78
+ ## Auth notes
79
+
80
+ - The v1 API requires both `Authorization: Bearer <PASETO>` and `X-Cortex-Actor`.
81
+ The CLI stamps both on every request — you never need to pass them by hand.
82
+ - 401s show the specific error code (`TOKEN_EXPIRED`, `actor_mismatch`, …)
83
+ and the remediation: usually `cortexdb init`.
84
+ - 403s cite the missing capability and the policy tier that denied. If a
85
+ recall returns `diagnostics.read denied`, pass `--diagnostics none` —
86
+ free-tier tokens don't carry the `diagnostics.read` capability.
87
+
88
+ ## License
89
+
90
+ Apache-2.0
@@ -0,0 +1,3 @@
1
+ """CortexDB CLI — command-line interface for the v1 memory API."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +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()
@@ -0,0 +1 @@
1
+ """CortexDB CLI command modules."""
@@ -0,0 +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)
@@ -0,0 +1,98 @@
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]")
@@ -0,0 +1,128 @@
1
+ """cortexdb auth — /v1/auth/{whoami,signup,tokens,revoke}."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from cortexdb_cli.client import DEFAULT_ENDPOINT, get_client, signup as signup_call
8
+ from cortexdb_cli.config import config_path, save_config
9
+ from cortexdb_cli.errors import handle_errors
10
+ from cortexdb_cli.output import console, is_json_mode, print_json, print_health, spinner
11
+ from cortexdb_cli.state import save_state, state_path
12
+
13
+
14
+ @click.group("auth")
15
+ def auth_group() -> None:
16
+ """Identity, token mint, revoke, anonymous signup."""
17
+
18
+
19
+ @auth_group.command("whoami")
20
+ @click.pass_context
21
+ @handle_errors
22
+ def auth_whoami(ctx: click.Context) -> None:
23
+ """GET /v1/auth/whoami — caller, tenant, effective capabilities."""
24
+ cfg = ctx.obj
25
+ with get_client(cfg) as client:
26
+ resp = client.get("/v1/auth/whoami")
27
+ resp.raise_for_status()
28
+ body = resp.json()
29
+ body.setdefault("status", "ok")
30
+ print_health(body, cfg)
31
+
32
+
33
+ @auth_group.command("signup")
34
+ @click.option(
35
+ "--endpoint",
36
+ default=DEFAULT_ENDPOINT,
37
+ show_default=True,
38
+ help="CortexDB v1 API base URL.",
39
+ )
40
+ @click.option("--save", "save_state_flag", is_flag=True, help="Persist to ~/.cortexdb/state.json")
41
+ @click.pass_context
42
+ @handle_errors
43
+ def auth_signup(ctx: click.Context, endpoint: str, save_state_flag: bool) -> None:
44
+ """POST /v1/auth/signup — anonymous free-tier token.
45
+
46
+ Without --save, prints the response and exits — useful when you want
47
+ to feed the token into a CI env var. With --save, behaves like
48
+ `cortexdb init` and writes ~/.cortexdb/state.json.
49
+ """
50
+ resp = signup_call(endpoint)
51
+ if save_state_flag:
52
+ save_state(
53
+ {
54
+ "token": resp["token"],
55
+ "user_id": resp["user_id"],
56
+ "scope": resp.get("scope") or resp["user_id"],
57
+ "expires_at": resp.get("expires_at"),
58
+ "tier": resp.get("tier", "free"),
59
+ "source": "signup",
60
+ }
61
+ )
62
+ save_config("default", {"endpoint": endpoint})
63
+ if is_json_mode(ctx.obj):
64
+ print_json(resp)
65
+ else:
66
+ console.print(
67
+ f"[success]Saved state[/success] to {state_path()}\n"
68
+ f" Actor: {resp['user_id']}\n"
69
+ f" Scope: {resp.get('scope') or resp['user_id']}\n"
70
+ f" Expires: {resp.get('expires_at', '?')}"
71
+ )
72
+ else:
73
+ print_json(resp)
74
+
75
+
76
+ @auth_group.command("tokens")
77
+ @click.option("--subject", required=True, help="`sub` claim on the minted token, e.g. user:alice")
78
+ @click.option(
79
+ "--ttl",
80
+ "ttl_seconds",
81
+ type=int,
82
+ default=3600,
83
+ show_default=True,
84
+ help="Token lifetime in seconds (server enforces a tenant ceiling).",
85
+ )
86
+ @click.option("--scope", "scopes", multiple=True, help="`scopes` claim — repeat for multiple.")
87
+ @click.pass_context
88
+ @handle_errors
89
+ def auth_tokens(
90
+ ctx: click.Context, subject: str, ttl_seconds: int, scopes: tuple[str, ...]
91
+ ) -> None:
92
+ """POST /v1/auth/tokens — mint a service-account token.
93
+
94
+ Requires the `auth.mint` capability. Free-tier anonymous tokens do not
95
+ have it — you must hold an IdP-issued token (or a token previously
96
+ minted by an account that does).
97
+
98
+ cortexdb auth tokens --subject user:alice --ttl 3600 --scope org:acme/user:alice
99
+ """
100
+ cfg = ctx.obj
101
+ body: dict[str, object] = {"subject": subject, "ttl_seconds": ttl_seconds}
102
+ if scopes:
103
+ body["scopes"] = list(scopes)
104
+ with get_client(cfg) as client:
105
+ if not is_json_mode(cfg):
106
+ with spinner("Minting..."):
107
+ resp = client.post("/v1/auth/tokens", json=body)
108
+ else:
109
+ resp = client.post("/v1/auth/tokens", json=body)
110
+ resp.raise_for_status()
111
+ print_json(resp.json())
112
+
113
+
114
+ @auth_group.command("revoke")
115
+ @click.option("--jti", required=True, help="Token jti claim to add to the revocation list.")
116
+ @click.option("--reason", required=True, help="Audit reason (e.g. 'token leaked').")
117
+ @click.pass_context
118
+ @handle_errors
119
+ def auth_revoke(ctx: click.Context, jti: str, reason: str) -> None:
120
+ """POST /v1/auth/revoke — invalidate a token by jti."""
121
+ cfg = ctx.obj
122
+ with get_client(cfg) as client:
123
+ resp = client.post("/v1/auth/revoke", json={"jti": jti, "reason": reason})
124
+ resp.raise_for_status()
125
+ if is_json_mode(cfg):
126
+ print_json({"revoked": jti})
127
+ else:
128
+ console.print(f"[success]Revoked[/success] jti={jti}")