cortexdb-cli 0.2.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 +3 -0
- cortexdb_cli/client.py +92 -0
- cortexdb_cli/commands/__init__.py +1 -0
- cortexdb_cli/commands/admin.py +83 -0
- cortexdb_cli/commands/answer.py +98 -0
- cortexdb_cli/commands/auth.py +128 -0
- cortexdb_cli/commands/beliefs.py +121 -0
- cortexdb_cli/commands/config_cmd.py +50 -0
- cortexdb_cli/commands/entities.py +174 -0
- cortexdb_cli/commands/episodes.py +131 -0
- cortexdb_cli/commands/experience.py +238 -0
- cortexdb_cli/commands/export_cmd.py +65 -0
- cortexdb_cli/commands/facts.py +171 -0
- cortexdb_cli/commands/forget.py +86 -0
- cortexdb_cli/commands/import_cmd.py +198 -0
- cortexdb_cli/commands/init.py +95 -0
- cortexdb_cli/commands/interactive.py +254 -0
- cortexdb_cli/commands/policy.py +99 -0
- cortexdb_cli/commands/recall.py +93 -0
- cortexdb_cli/commands/scopes.py +98 -0
- cortexdb_cli/commands/search.py +64 -0
- cortexdb_cli/commands/understanding.py +141 -0
- cortexdb_cli/config.py +113 -0
- cortexdb_cli/errors.py +96 -0
- cortexdb_cli/main.py +140 -0
- cortexdb_cli/output.py +399 -0
- cortexdb_cli/state.py +53 -0
- cortexdb_cli-0.2.0.dist-info/METADATA +122 -0
- cortexdb_cli-0.2.0.dist-info/RECORD +31 -0
- cortexdb_cli-0.2.0.dist-info/WHEEL +4 -0
- cortexdb_cli-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""cortexdb understanding — GET /v1/understanding* + POST /v1/understanding/synthesize."""
|
|
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
|
+
console,
|
|
11
|
+
escape,
|
|
12
|
+
is_json_mode,
|
|
13
|
+
print_json,
|
|
14
|
+
spinner,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group("understanding")
|
|
19
|
+
def understanding_group() -> None:
|
|
20
|
+
"""Understanding — LLM-synthesized concepts and themes across a scope."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _scope_or_die(cfg: dict, scope: str | None) -> str:
|
|
24
|
+
s = scope or cfg.get("scope") or cfg.get("actor")
|
|
25
|
+
if not s:
|
|
26
|
+
raise click.UsageError(
|
|
27
|
+
"No scope configured. Run `cortexdb init` first, or pass --scope explicitly."
|
|
28
|
+
)
|
|
29
|
+
return s
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@understanding_group.command("list")
|
|
33
|
+
@click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
|
|
34
|
+
@click.option("--topic", help="Filter by topic name")
|
|
35
|
+
@click.option("--limit", "-l", default=50, show_default=True)
|
|
36
|
+
@click.pass_context
|
|
37
|
+
@handle_errors
|
|
38
|
+
def understanding_list(
|
|
39
|
+
ctx: click.Context, scope: str | None, topic: str | None, limit: int
|
|
40
|
+
) -> None:
|
|
41
|
+
"""List synthesized concepts."""
|
|
42
|
+
cfg = ctx.obj
|
|
43
|
+
s = _scope_or_die(cfg, scope)
|
|
44
|
+
params: dict[str, str | int] = {"scope": s, "limit": limit}
|
|
45
|
+
if topic:
|
|
46
|
+
params["topic"] = topic
|
|
47
|
+
with get_client(cfg) as client:
|
|
48
|
+
if not is_json_mode(cfg):
|
|
49
|
+
with spinner("Loading concepts..."):
|
|
50
|
+
resp = client.get("/v1/understanding", params=params)
|
|
51
|
+
else:
|
|
52
|
+
resp = client.get("/v1/understanding", params=params)
|
|
53
|
+
resp.raise_for_status()
|
|
54
|
+
|
|
55
|
+
data = resp.json()
|
|
56
|
+
if is_json_mode(cfg):
|
|
57
|
+
print_json(data)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
items = data.get("items", data) if isinstance(data, dict) else data
|
|
61
|
+
if not items:
|
|
62
|
+
console.print(
|
|
63
|
+
"[dim]No concepts synthesised yet. Try [bold]cortexdb understanding synthesize[/bold] "
|
|
64
|
+
"to trigger a pass.[/dim]"
|
|
65
|
+
)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
from rich.table import Table
|
|
69
|
+
|
|
70
|
+
table = Table(title="[header]Understanding[/header]", border_style="blue")
|
|
71
|
+
table.add_column("Concept", style="bold", max_width=24)
|
|
72
|
+
table.add_column("Topic", style="magenta", max_width=20)
|
|
73
|
+
table.add_column("Summary", max_width=60)
|
|
74
|
+
table.add_column("Conf", justify="right", width=6)
|
|
75
|
+
for c in items:
|
|
76
|
+
table.add_row(
|
|
77
|
+
escape(str(c.get("name") or c.get("concept_id", ""))[:24]),
|
|
78
|
+
escape(str(c.get("topic", ""))),
|
|
79
|
+
escape(str(c.get("summary", ""))[:120]),
|
|
80
|
+
f"{(c.get('confidence') or 0):.0%}",
|
|
81
|
+
)
|
|
82
|
+
console.print(table)
|
|
83
|
+
if isinstance(data, dict) and data.get("_partial"):
|
|
84
|
+
console.print(
|
|
85
|
+
"[dim]_partial: true — the synthesizer is still catching up on this scope.[/dim]"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@understanding_group.command("coverage")
|
|
90
|
+
@click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
|
|
91
|
+
@click.pass_context
|
|
92
|
+
@handle_errors
|
|
93
|
+
def understanding_coverage(ctx: click.Context, scope: str | None) -> None:
|
|
94
|
+
"""Per-topic synthesis coverage and confidence."""
|
|
95
|
+
cfg = ctx.obj
|
|
96
|
+
s = _scope_or_die(cfg, scope)
|
|
97
|
+
with get_client(cfg) as client:
|
|
98
|
+
if not is_json_mode(cfg):
|
|
99
|
+
with spinner("Loading coverage..."):
|
|
100
|
+
resp = client.get("/v1/understanding/coverage", params={"scope": s})
|
|
101
|
+
else:
|
|
102
|
+
resp = client.get("/v1/understanding/coverage", params={"scope": s})
|
|
103
|
+
resp.raise_for_status()
|
|
104
|
+
print_json(resp.json())
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@understanding_group.command("synthesize")
|
|
108
|
+
@click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
|
|
109
|
+
@click.option("--topic", "topics", multiple=True, help="Restrict to topic(s); repeat for multiple.")
|
|
110
|
+
@click.option("--force", is_flag=True, help="Force re-synthesis even if already current.")
|
|
111
|
+
@click.pass_context
|
|
112
|
+
@handle_errors
|
|
113
|
+
def understanding_synthesize(
|
|
114
|
+
ctx: click.Context, scope: str | None, topics: tuple[str, ...], force: bool
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Trigger an on-demand synthesis pass.
|
|
117
|
+
|
|
118
|
+
Calls an LLM (Claude Opus 4.6 by default) — counts against the
|
|
119
|
+
tenant's LLM quota. Capability `understanding.synthesize` required.
|
|
120
|
+
The free tier denies this; paid tiers allow it.
|
|
121
|
+
|
|
122
|
+
cortexdb understanding synthesize --topic sales_process
|
|
123
|
+
"""
|
|
124
|
+
cfg = ctx.obj
|
|
125
|
+
s = _scope_or_die(cfg, scope)
|
|
126
|
+
body: dict[str, object] = {"scope": s, "force": force}
|
|
127
|
+
if topics:
|
|
128
|
+
body["topics"] = list(topics)
|
|
129
|
+
with get_client(cfg) as client:
|
|
130
|
+
resp = client.post("/v1/understanding/synthesize", json=body)
|
|
131
|
+
resp.raise_for_status()
|
|
132
|
+
data = resp.json()
|
|
133
|
+
if is_json_mode(cfg):
|
|
134
|
+
print_json(data)
|
|
135
|
+
else:
|
|
136
|
+
console.print(
|
|
137
|
+
f"[success]Synthesis scheduled[/success] — job [bold]{data.get('job_id', '?')}[/bold]"
|
|
138
|
+
)
|
|
139
|
+
stream = data.get("lifecycle_stream")
|
|
140
|
+
if stream:
|
|
141
|
+
console.print(f"[dim]Watch: {escape(stream)}[/dim]")
|
cortexdb_cli/config.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Configuration management for CortexDB CLI.
|
|
2
|
+
|
|
3
|
+
Two files live under `~/.cortexdb/`:
|
|
4
|
+
- `config.toml` — human-editable: endpoint, profile names.
|
|
5
|
+
- `state.json` — machine-managed: PASETO token, actor, scope, expiry
|
|
6
|
+
(written by `cortexdb init`; never edited by hand).
|
|
7
|
+
|
|
8
|
+
`load_config` merges everything together so commands see one dict:
|
|
9
|
+
|
|
10
|
+
endpoint ← env CORTEXDB_URL > config.toml > default
|
|
11
|
+
api_key ← env CORTEXDB_API_KEY > state.json[token] > config.toml
|
|
12
|
+
actor ← env CORTEXDB_ACTOR > state.json[user_id] > config.toml
|
|
13
|
+
scope ← env CORTEXDB_SCOPE > state.json[scope] > config.toml > actor
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
if sys.version_info >= (3, 11):
|
|
24
|
+
import tomllib
|
|
25
|
+
else:
|
|
26
|
+
import tomli as tomllib
|
|
27
|
+
|
|
28
|
+
import tomli_w
|
|
29
|
+
|
|
30
|
+
DEFAULT_ENDPOINT = "https://api-v1.cortexdb.ai"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def config_dir() -> Path:
|
|
34
|
+
"""Return the CortexDB config directory, creating it if needed."""
|
|
35
|
+
d = Path.home() / ".cortexdb"
|
|
36
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
return d
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def config_path() -> Path:
|
|
41
|
+
return config_dir() / "config.toml"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def history_path() -> Path:
|
|
45
|
+
return config_dir() / "history"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_config(profile: str = "default") -> dict[str, Any]:
|
|
49
|
+
"""Load merged config: TOML file + state.json + env vars."""
|
|
50
|
+
cfg: dict[str, Any] = {}
|
|
51
|
+
path = config_path()
|
|
52
|
+
if path.exists():
|
|
53
|
+
with open(path, "rb") as f:
|
|
54
|
+
data = tomllib.load(f)
|
|
55
|
+
if profile != "default" and profile in data.get("profiles", {}):
|
|
56
|
+
cfg = dict(data["profiles"][profile])
|
|
57
|
+
elif "default" in data:
|
|
58
|
+
cfg = dict(data["default"])
|
|
59
|
+
else:
|
|
60
|
+
cfg = {k: v for k, v in data.items() if k != "profiles"}
|
|
61
|
+
|
|
62
|
+
# Merge persisted signup state.
|
|
63
|
+
# Import here to avoid a circular import (state.py imports config_dir).
|
|
64
|
+
from cortexdb_cli.state import load_state
|
|
65
|
+
|
|
66
|
+
st = load_state()
|
|
67
|
+
if st.get("token") and not cfg.get("api_key"):
|
|
68
|
+
cfg["api_key"] = st["token"]
|
|
69
|
+
if st.get("user_id") and not cfg.get("actor"):
|
|
70
|
+
cfg["actor"] = st["user_id"]
|
|
71
|
+
if st.get("scope") and not cfg.get("scope"):
|
|
72
|
+
cfg["scope"] = st["scope"]
|
|
73
|
+
if st.get("expires_at") and not cfg.get("expires_at"):
|
|
74
|
+
cfg["expires_at"] = st["expires_at"]
|
|
75
|
+
|
|
76
|
+
# Env vars win.
|
|
77
|
+
if os.environ.get("CORTEXDB_API_KEY"):
|
|
78
|
+
cfg["api_key"] = os.environ["CORTEXDB_API_KEY"]
|
|
79
|
+
if os.environ.get("CORTEXDB_URL"):
|
|
80
|
+
cfg["endpoint"] = os.environ["CORTEXDB_URL"]
|
|
81
|
+
if os.environ.get("CORTEXDB_ENDPOINT"):
|
|
82
|
+
cfg["endpoint"] = os.environ["CORTEXDB_ENDPOINT"]
|
|
83
|
+
if os.environ.get("CORTEXDB_ACTOR"):
|
|
84
|
+
cfg["actor"] = os.environ["CORTEXDB_ACTOR"]
|
|
85
|
+
if os.environ.get("CORTEXDB_SCOPE"):
|
|
86
|
+
cfg["scope"] = os.environ["CORTEXDB_SCOPE"]
|
|
87
|
+
|
|
88
|
+
cfg.setdefault("endpoint", DEFAULT_ENDPOINT)
|
|
89
|
+
# Scope defaults to actor when not explicitly set.
|
|
90
|
+
if cfg.get("actor") and not cfg.get("scope"):
|
|
91
|
+
cfg["scope"] = cfg["actor"]
|
|
92
|
+
return cfg
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def save_config(profile: str, data: dict[str, Any]) -> None:
|
|
96
|
+
"""Save the human-editable TOML config for a profile."""
|
|
97
|
+
path = config_path()
|
|
98
|
+
existing: dict[str, Any] = {}
|
|
99
|
+
if path.exists():
|
|
100
|
+
with open(path, "rb") as f:
|
|
101
|
+
existing = tomllib.load(f)
|
|
102
|
+
|
|
103
|
+
if profile == "default":
|
|
104
|
+
existing["default"] = data
|
|
105
|
+
else:
|
|
106
|
+
existing.setdefault("profiles", {})[profile] = data
|
|
107
|
+
|
|
108
|
+
with open(path, "wb") as f:
|
|
109
|
+
tomli_w.dump(existing, f)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_value(cfg: dict[str, Any], key: str, default: Any = None) -> Any:
|
|
113
|
+
return cfg.get(key, default)
|
cortexdb_cli/errors.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Error handling for CortexDB CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from cortexdb_cli.output import err_console
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def escape_safe(s: Any) -> str:
|
|
16
|
+
"""Rich-safe escape used in the error decorator (avoids circular imports)."""
|
|
17
|
+
from rich.markup import escape
|
|
18
|
+
|
|
19
|
+
return escape(str(s))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def handle_errors(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
23
|
+
"""Decorator that catches common errors and prints friendly messages."""
|
|
24
|
+
|
|
25
|
+
@functools.wraps(fn)
|
|
26
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
27
|
+
try:
|
|
28
|
+
return fn(*args, **kwargs)
|
|
29
|
+
except httpx.HTTPStatusError as exc:
|
|
30
|
+
status = exc.response.status_code
|
|
31
|
+
body: dict[str, Any] = {}
|
|
32
|
+
try:
|
|
33
|
+
body = exc.response.json()
|
|
34
|
+
except Exception:
|
|
35
|
+
body = {}
|
|
36
|
+
code = body.get("error_code") or body.get("code") or ""
|
|
37
|
+
msg = body.get("message") or body.get("error") or exc.response.text[:200]
|
|
38
|
+
|
|
39
|
+
if status == 401:
|
|
40
|
+
if code in ("TOKEN_EXPIRED", "INVALID_TOKEN_SIGNATURE", "MISSING_TOKEN"):
|
|
41
|
+
err_console.print(
|
|
42
|
+
f"[error]Token rejected ({code}).[/error] "
|
|
43
|
+
"Run [bold]cortexdb init[/bold] to mint a fresh one."
|
|
44
|
+
)
|
|
45
|
+
elif code == "actor_mismatch":
|
|
46
|
+
err_console.print(
|
|
47
|
+
"[error]Actor mismatch.[/error] The X-Cortex-Actor header "
|
|
48
|
+
"doesn't match the token's subject. Re-run "
|
|
49
|
+
"[bold]cortexdb init[/bold] to re-bind."
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
err_console.print(
|
|
53
|
+
f"[error]Authentication failed:[/error] {msg}\n"
|
|
54
|
+
"Run [bold]cortexdb init[/bold] to set up credentials."
|
|
55
|
+
)
|
|
56
|
+
elif status == 403:
|
|
57
|
+
# The v1 surface always cites the missing capability.
|
|
58
|
+
details = body.get("details") or {}
|
|
59
|
+
cap = details.get("capability") or ""
|
|
60
|
+
tier = details.get("decided_by_tier") or ""
|
|
61
|
+
if cap:
|
|
62
|
+
err_console.print(
|
|
63
|
+
f"[error]Denied by {escape_safe(tier)}:[/error] "
|
|
64
|
+
f"missing capability [bold]{escape_safe(cap)}[/bold]."
|
|
65
|
+
)
|
|
66
|
+
if cap == "diagnostics.read":
|
|
67
|
+
err_console.print(
|
|
68
|
+
"[dim]Hint: pass [bold]--diagnostics none[/bold] (free tier).[/dim]"
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
err_console.print(f"[error]Forbidden:[/error] {msg}")
|
|
72
|
+
elif status == 429:
|
|
73
|
+
retry = exc.response.headers.get("Retry-After", "?")
|
|
74
|
+
err_console.print(f"[error]Rate limited.[/error] Retry after {retry}s.")
|
|
75
|
+
elif status == 402:
|
|
76
|
+
err_console.print(f"[warning]Feature not available on your tier.[/warning] {msg}")
|
|
77
|
+
else:
|
|
78
|
+
err_console.print(f"[error]API error (HTTP {status}, {code}):[/error] {msg}")
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
except httpx.ConnectError:
|
|
81
|
+
err_console.print(
|
|
82
|
+
"[error]Cannot connect to CortexDB.[/error] "
|
|
83
|
+
"Check your endpoint and network."
|
|
84
|
+
)
|
|
85
|
+
sys.exit(2)
|
|
86
|
+
except httpx.TimeoutException:
|
|
87
|
+
err_console.print("[error]Request timed out.[/error]")
|
|
88
|
+
sys.exit(2)
|
|
89
|
+
except click.Abort:
|
|
90
|
+
err_console.print("\n[dim]Aborted.[/dim]")
|
|
91
|
+
sys.exit(130)
|
|
92
|
+
except KeyboardInterrupt:
|
|
93
|
+
err_console.print("\n[dim]Interrupted.[/dim]")
|
|
94
|
+
sys.exit(130)
|
|
95
|
+
|
|
96
|
+
return wrapper
|
cortexdb_cli/main.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""CortexDB CLI — the v1 memory API at your fingertips.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
cortexdb init anonymous signup; persist token + actor
|
|
5
|
+
cortexdb experience "..." store a memory (POST /v1/experience)
|
|
6
|
+
cortexdb experience bulk file.jsonl bulk ingest (POST /v1/experience/bulk)
|
|
7
|
+
cortexdb recall "..." recall context pack (POST /v1/recall)
|
|
8
|
+
cortexdb search "..." granular event hits (POST /v1/recall, granular)
|
|
9
|
+
cortexdb answer "..." recall + LLM answer (POST /v1/answer)
|
|
10
|
+
cortexdb forget --memory-id ... delete with audit (POST /v1/forget)
|
|
11
|
+
cortexdb episodes {list,get,delete}
|
|
12
|
+
cortexdb entities {list,get,link}
|
|
13
|
+
cortexdb facts {list,timeline}
|
|
14
|
+
cortexdb beliefs {list,why,build}
|
|
15
|
+
cortexdb understanding {list,coverage,synthesize}
|
|
16
|
+
cortexdb scopes {list,register,members}
|
|
17
|
+
cortexdb auth {whoami,signup,tokens,revoke}
|
|
18
|
+
cortexdb policy {effective,deployment}
|
|
19
|
+
cortexdb admin {health,usage,layers}
|
|
20
|
+
cortexdb import data.json
|
|
21
|
+
cortexdb export -o backup.json
|
|
22
|
+
cortexdb config {show,set}
|
|
23
|
+
cortexdb interactive REPL
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import sys
|
|
29
|
+
|
|
30
|
+
import click
|
|
31
|
+
|
|
32
|
+
from cortexdb_cli import __version__
|
|
33
|
+
from cortexdb_cli.client import DEFAULT_ENDPOINT
|
|
34
|
+
from cortexdb_cli.config import load_config
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
import rich_click
|
|
38
|
+
|
|
39
|
+
rich_click.rich_click.SHOW_ARGUMENTS = True
|
|
40
|
+
rich_click.rich_click.GROUP_ARGUMENTS_OPTIONS = True
|
|
41
|
+
rich_click.rich_click.STYLE_COMMANDS_TABLE_COLUMN_WIDTH_RATIO = (1, 2)
|
|
42
|
+
rich_click.rich_click.USE_MARKDOWN = True
|
|
43
|
+
except ImportError:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@click.group(invoke_without_command=True, context_settings=CONTEXT_SETTINGS)
|
|
51
|
+
@click.option("--api-key", envvar="CORTEXDB_API_KEY", help="PASETO bearer token (or env CORTEXDB_API_KEY)")
|
|
52
|
+
@click.option("--endpoint", envvar="CORTEXDB_ENDPOINT", help="API base URL (default api-v1.cortexdb.ai)")
|
|
53
|
+
@click.option("--actor", envvar="CORTEXDB_ACTOR", help="X-Cortex-Actor (or env CORTEXDB_ACTOR)")
|
|
54
|
+
@click.option("--scope", envvar="CORTEXDB_SCOPE", help="Default scope path (or env CORTEXDB_SCOPE)")
|
|
55
|
+
@click.option("--json", "json_output", is_flag=True, help="Output as JSON (auto-enabled when piped)")
|
|
56
|
+
@click.option("--profile", "-p", default="default", help="Config profile")
|
|
57
|
+
@click.version_option(version=__version__, prog_name="cortexdb")
|
|
58
|
+
@click.pass_context
|
|
59
|
+
def cli(
|
|
60
|
+
ctx: click.Context,
|
|
61
|
+
api_key: str | None,
|
|
62
|
+
endpoint: str | None,
|
|
63
|
+
actor: str | None,
|
|
64
|
+
scope: str | None,
|
|
65
|
+
json_output: bool,
|
|
66
|
+
profile: str,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""CortexDB — long-term memory layer for AI systems.
|
|
69
|
+
|
|
70
|
+
Run without a command to enter the interactive REPL.
|
|
71
|
+
"""
|
|
72
|
+
ctx.ensure_object(dict)
|
|
73
|
+
|
|
74
|
+
cfg = load_config(profile)
|
|
75
|
+
ctx.obj["api_key"] = api_key or cfg.get("api_key", "")
|
|
76
|
+
ctx.obj["endpoint"] = endpoint or cfg.get("endpoint") or DEFAULT_ENDPOINT
|
|
77
|
+
ctx.obj["actor"] = actor or cfg.get("actor", "")
|
|
78
|
+
ctx.obj["scope"] = scope or cfg.get("scope") or ctx.obj["actor"]
|
|
79
|
+
ctx.obj["expires_at"] = cfg.get("expires_at")
|
|
80
|
+
ctx.obj["json_output"] = json_output or not sys.stdout.isatty()
|
|
81
|
+
ctx.obj["profile"] = profile
|
|
82
|
+
|
|
83
|
+
# No subcommand → interactive REPL.
|
|
84
|
+
if ctx.invoked_subcommand is None:
|
|
85
|
+
if not sys.stdout.isatty():
|
|
86
|
+
click.echo(ctx.get_help())
|
|
87
|
+
return
|
|
88
|
+
from cortexdb_cli.commands.interactive import repl
|
|
89
|
+
|
|
90
|
+
repl(ctx)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --- Register commands ---
|
|
94
|
+
|
|
95
|
+
from cortexdb_cli.commands.admin import admin_group
|
|
96
|
+
from cortexdb_cli.commands.answer import answer_cmd
|
|
97
|
+
from cortexdb_cli.commands.auth import auth_group
|
|
98
|
+
from cortexdb_cli.commands.beliefs import beliefs_group
|
|
99
|
+
from cortexdb_cli.commands.config_cmd import config_group
|
|
100
|
+
from cortexdb_cli.commands.entities import entities_group
|
|
101
|
+
from cortexdb_cli.commands.episodes import episodes_group
|
|
102
|
+
from cortexdb_cli.commands.experience import experience_cmd, remember_alias
|
|
103
|
+
from cortexdb_cli.commands.export_cmd import export_cmd
|
|
104
|
+
from cortexdb_cli.commands.facts import facts_group
|
|
105
|
+
from cortexdb_cli.commands.forget import forget_cmd
|
|
106
|
+
from cortexdb_cli.commands.import_cmd import import_cmd
|
|
107
|
+
from cortexdb_cli.commands.init import init_cmd
|
|
108
|
+
from cortexdb_cli.commands.policy import policy_group
|
|
109
|
+
from cortexdb_cli.commands.recall import recall_cmd
|
|
110
|
+
from cortexdb_cli.commands.scopes import scopes_group
|
|
111
|
+
from cortexdb_cli.commands.search import search_cmd
|
|
112
|
+
from cortexdb_cli.commands.understanding import understanding_group
|
|
113
|
+
|
|
114
|
+
cli.add_command(init_cmd)
|
|
115
|
+
cli.add_command(experience_cmd)
|
|
116
|
+
cli.add_command(remember_alias) # hidden alias for muscle memory
|
|
117
|
+
cli.add_command(recall_cmd)
|
|
118
|
+
cli.add_command(search_cmd)
|
|
119
|
+
cli.add_command(answer_cmd)
|
|
120
|
+
cli.add_command(forget_cmd)
|
|
121
|
+
cli.add_command(episodes_group)
|
|
122
|
+
cli.add_command(entities_group)
|
|
123
|
+
cli.add_command(facts_group)
|
|
124
|
+
cli.add_command(beliefs_group)
|
|
125
|
+
cli.add_command(understanding_group)
|
|
126
|
+
cli.add_command(scopes_group)
|
|
127
|
+
cli.add_command(auth_group)
|
|
128
|
+
cli.add_command(policy_group)
|
|
129
|
+
cli.add_command(admin_group)
|
|
130
|
+
cli.add_command(import_cmd)
|
|
131
|
+
cli.add_command(export_cmd)
|
|
132
|
+
cli.add_command(config_group)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main() -> None:
|
|
136
|
+
cli()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
main()
|