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,254 @@
|
|
|
1
|
+
"""cortexdb interactive REPL — chat with your memory.
|
|
2
|
+
|
|
3
|
+
Wraps the same v1 endpoints the discrete commands hit, so anything you
|
|
4
|
+
can do via subcommand you can do here, plus natural-language input routed
|
|
5
|
+
through /v1/recall.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from cortexdb_cli.client import get_client
|
|
15
|
+
from cortexdb_cli.output import console, print_recall, print_search, print_error
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
BANNER = """
|
|
19
|
+
[bold magenta] ____ _ ____ ____ [/bold magenta]
|
|
20
|
+
[bold magenta] / ___|___ _ __| |_ _____ _| _ \\| __ ) [/bold magenta]
|
|
21
|
+
[bold magenta]| | / _ \\| '__| __/ _ \\ \\/ / | | | _ \\ [/bold magenta]
|
|
22
|
+
[bold magenta]| |__| (_) | | | || __/> <| |_| | |_) |[/bold magenta]
|
|
23
|
+
[bold magenta] \\____\\___/|_| \\__\\___/_/\\_\\____/|____/ [/bold magenta]
|
|
24
|
+
|
|
25
|
+
[dim]Type a question to recall from memory.[/dim]
|
|
26
|
+
[dim]Commands: /remember /recall /search /forget /health /usage /help /quit[/dim]
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
HELP_TEXT = """
|
|
30
|
+
[bold]Commands:[/bold]
|
|
31
|
+
[cyan]/remember TEXT[/cyan] Store a new memory
|
|
32
|
+
[cyan]/recall QUERY[/cyan] Recall a stratified context pack
|
|
33
|
+
[cyan]/search QUERY[/cyan] Granular search — individual event hits
|
|
34
|
+
[cyan]/forget QUERY[/cyan] Forget by query (prompts for reason)
|
|
35
|
+
[cyan]/health[/cyan] Identity + reachability via /v1/auth/whoami
|
|
36
|
+
[cyan]/usage[/cyan] Tier, caps, rate-limit headers
|
|
37
|
+
[cyan]/help[/cyan] This help
|
|
38
|
+
[cyan]/quit[/cyan] Exit
|
|
39
|
+
|
|
40
|
+
[bold]Natural Language:[/bold]
|
|
41
|
+
Just type a question to recall from memory.
|
|
42
|
+
e.g., "what happened with the deploy?"
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _now_iso() -> str:
|
|
47
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _idem(content: str, scope: str) -> str:
|
|
51
|
+
return "cli-repl:" + hashlib.sha256(
|
|
52
|
+
f"{scope}|{content}".encode("utf-8")
|
|
53
|
+
).hexdigest()[:16]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _experience_body(scope: str, text: str) -> dict[str, Any]:
|
|
57
|
+
return {
|
|
58
|
+
"scope": scope,
|
|
59
|
+
"modality": "conversation",
|
|
60
|
+
"content": {"kind": "message", "role": "user", "text": text},
|
|
61
|
+
"context": {"observed_at": _now_iso()},
|
|
62
|
+
"idempotency_key": _idem(text, scope),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def repl(ctx: Any) -> None:
|
|
67
|
+
"""Run the interactive REPL."""
|
|
68
|
+
try:
|
|
69
|
+
from prompt_toolkit import PromptSession
|
|
70
|
+
from prompt_toolkit.history import FileHistory
|
|
71
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
72
|
+
from prompt_toolkit.formatted_text import HTML
|
|
73
|
+
except ImportError:
|
|
74
|
+
console.print("[error]prompt-toolkit is required for interactive mode.[/error]")
|
|
75
|
+
console.print("Install it: [bold]pip install prompt-toolkit[/bold]")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
from cortexdb_cli.config import history_path
|
|
79
|
+
from cortexdb_cli.output import (
|
|
80
|
+
print_health,
|
|
81
|
+
print_usage,
|
|
82
|
+
print_remember,
|
|
83
|
+
print_forget,
|
|
84
|
+
spinner,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
cfg = ctx.obj
|
|
88
|
+
scope = cfg.get("scope") or cfg.get("actor")
|
|
89
|
+
if not scope:
|
|
90
|
+
console.print(
|
|
91
|
+
"[warning]No scope configured. Run [bold]cortexdb init[/bold] first.[/warning]"
|
|
92
|
+
)
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
console.print(BANNER)
|
|
96
|
+
|
|
97
|
+
session: PromptSession[str] = PromptSession(
|
|
98
|
+
history=FileHistory(str(history_path())),
|
|
99
|
+
auto_suggest=AutoSuggestFromHistory(),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
with get_client(cfg) as client:
|
|
103
|
+
while True:
|
|
104
|
+
try:
|
|
105
|
+
text = session.prompt(
|
|
106
|
+
HTML("<ansicyan><b>cortexdb</b></ansicyan><b>></b> "),
|
|
107
|
+
).strip()
|
|
108
|
+
except (EOFError, KeyboardInterrupt):
|
|
109
|
+
console.print("\n[dim]Goodbye.[/dim]")
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
if not text:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
if text.lower() in ("/quit", "/exit", "/q"):
|
|
116
|
+
console.print("[dim]Goodbye.[/dim]")
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
elif text.lower() == "/help":
|
|
120
|
+
console.print(HELP_TEXT)
|
|
121
|
+
|
|
122
|
+
elif text.lower() == "/health":
|
|
123
|
+
try:
|
|
124
|
+
with spinner("Checking..."):
|
|
125
|
+
resp = client.get("/v1/auth/whoami")
|
|
126
|
+
resp.raise_for_status()
|
|
127
|
+
body = resp.json()
|
|
128
|
+
body.setdefault("status", "ok")
|
|
129
|
+
print_health(body, cfg)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
print_error(str(e))
|
|
132
|
+
|
|
133
|
+
elif text.lower() == "/usage":
|
|
134
|
+
try:
|
|
135
|
+
with spinner("Loading usage..."):
|
|
136
|
+
resp = client.get("/v1/auth/whoami")
|
|
137
|
+
resp.raise_for_status()
|
|
138
|
+
body = resp.json()
|
|
139
|
+
for k in (
|
|
140
|
+
"X-RateLimit-Limit",
|
|
141
|
+
"X-RateLimit-Reset",
|
|
142
|
+
"X-Cortex-Token-Expires-In",
|
|
143
|
+
"X-Cortex-Token-Expires-At",
|
|
144
|
+
):
|
|
145
|
+
if k in resp.headers:
|
|
146
|
+
body[k] = resp.headers[k]
|
|
147
|
+
print_usage(body, cfg)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
print_error(str(e))
|
|
150
|
+
|
|
151
|
+
elif text.lower().startswith("/remember "):
|
|
152
|
+
content = text[10:].strip()
|
|
153
|
+
if not content:
|
|
154
|
+
console.print("[warning]Usage: /remember TEXT[/warning]")
|
|
155
|
+
continue
|
|
156
|
+
try:
|
|
157
|
+
with spinner("Storing..."):
|
|
158
|
+
resp = client.post("/v1/experience", json=_experience_body(scope, content))
|
|
159
|
+
resp.raise_for_status()
|
|
160
|
+
print_remember(resp.json(), cfg)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
print_error(str(e))
|
|
163
|
+
|
|
164
|
+
elif text.lower().startswith("/recall "):
|
|
165
|
+
query = text[8:].strip()
|
|
166
|
+
if not query:
|
|
167
|
+
console.print("[warning]Usage: /recall QUERY[/warning]")
|
|
168
|
+
continue
|
|
169
|
+
try:
|
|
170
|
+
with spinner("Recalling..."):
|
|
171
|
+
resp = client.post(
|
|
172
|
+
"/v1/recall",
|
|
173
|
+
json={
|
|
174
|
+
"scope": scope,
|
|
175
|
+
"view": "holistic",
|
|
176
|
+
"query": query,
|
|
177
|
+
"diagnostics": "none",
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
resp.raise_for_status()
|
|
181
|
+
print_recall(resp.json(), cfg)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
print_error(str(e))
|
|
184
|
+
|
|
185
|
+
elif text.lower().startswith("/search "):
|
|
186
|
+
query = text[8:].strip()
|
|
187
|
+
if not query:
|
|
188
|
+
console.print("[warning]Usage: /search QUERY[/warning]")
|
|
189
|
+
continue
|
|
190
|
+
try:
|
|
191
|
+
with spinner("Searching..."):
|
|
192
|
+
resp = client.post(
|
|
193
|
+
"/v1/recall",
|
|
194
|
+
json={
|
|
195
|
+
"scope": scope,
|
|
196
|
+
"view": "granular",
|
|
197
|
+
"query": query,
|
|
198
|
+
"diagnostics": "none",
|
|
199
|
+
"budgets": {"max_events": 10},
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
resp.raise_for_status()
|
|
203
|
+
print_search(resp.json(), cfg)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
print_error(str(e))
|
|
206
|
+
|
|
207
|
+
elif text.lower().startswith("/forget "):
|
|
208
|
+
query = text[8:].strip()
|
|
209
|
+
if not query:
|
|
210
|
+
console.print("[warning]Usage: /forget QUERY[/warning]")
|
|
211
|
+
continue
|
|
212
|
+
from prompt_toolkit import prompt as pt_prompt
|
|
213
|
+
|
|
214
|
+
reason = pt_prompt("Reason: ")
|
|
215
|
+
if not reason.strip():
|
|
216
|
+
console.print("[warning]A reason is required.[/warning]")
|
|
217
|
+
continue
|
|
218
|
+
try:
|
|
219
|
+
with spinner("Forgetting..."):
|
|
220
|
+
resp = client.post(
|
|
221
|
+
"/v1/forget",
|
|
222
|
+
json={
|
|
223
|
+
"scope": scope,
|
|
224
|
+
"selector": {"query": query},
|
|
225
|
+
"cascade": "derived_only",
|
|
226
|
+
"audit_note": reason.strip(),
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
resp.raise_for_status()
|
|
230
|
+
print_forget(resp.json(), cfg)
|
|
231
|
+
except Exception as e:
|
|
232
|
+
print_error(str(e))
|
|
233
|
+
|
|
234
|
+
elif text.startswith("/"):
|
|
235
|
+
console.print(f"[warning]Unknown command: {text.split()[0]}[/warning]")
|
|
236
|
+
console.print("[dim]Type /help for available commands.[/dim]")
|
|
237
|
+
|
|
238
|
+
else:
|
|
239
|
+
# Natural language -> /v1/recall holistic
|
|
240
|
+
try:
|
|
241
|
+
with spinner("Thinking..."):
|
|
242
|
+
resp = client.post(
|
|
243
|
+
"/v1/recall",
|
|
244
|
+
json={
|
|
245
|
+
"scope": scope,
|
|
246
|
+
"view": "holistic",
|
|
247
|
+
"query": text,
|
|
248
|
+
"diagnostics": "none",
|
|
249
|
+
},
|
|
250
|
+
)
|
|
251
|
+
resp.raise_for_status()
|
|
252
|
+
print_recall(resp.json(), cfg)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
print_error(str(e))
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""cortexdb policy — /v1/policy/effective + /v1/policy/deployment."""
|
|
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("policy")
|
|
19
|
+
def policy_group() -> None:
|
|
20
|
+
"""Policy introspection — what is this actor allowed to do, and where?"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@policy_group.command("effective")
|
|
24
|
+
@click.option("--actor", "actor_arg", help="Actor to query (defaults to your actor).")
|
|
25
|
+
@click.option("--scope", "-S", help="Scope path (defaults to your default scope).")
|
|
26
|
+
@click.pass_context
|
|
27
|
+
@handle_errors
|
|
28
|
+
def policy_effective(
|
|
29
|
+
ctx: click.Context, actor_arg: str | None, scope: str | None
|
|
30
|
+
) -> None:
|
|
31
|
+
"""GET /v1/policy/effective — full allow/deny matrix.
|
|
32
|
+
|
|
33
|
+
Shows every capability and which policy tier (deployment / tenant /
|
|
34
|
+
scope / actor) decided each one. The authoritative answer to "what
|
|
35
|
+
can <actor> do at <scope> right now."
|
|
36
|
+
|
|
37
|
+
cortexdb policy effective --actor user:alice --scope org:acme/user:alice
|
|
38
|
+
"""
|
|
39
|
+
cfg = ctx.obj
|
|
40
|
+
actor = actor_arg or cfg.get("actor")
|
|
41
|
+
target_scope = scope or cfg.get("scope") or actor
|
|
42
|
+
if not actor or not target_scope:
|
|
43
|
+
raise click.UsageError(
|
|
44
|
+
"Need both --actor and --scope (or run `cortexdb init` so defaults are set)."
|
|
45
|
+
)
|
|
46
|
+
params = {"actor": actor, "scope": target_scope}
|
|
47
|
+
with get_client(cfg) as client:
|
|
48
|
+
if not is_json_mode(cfg):
|
|
49
|
+
with spinner("Loading policy..."):
|
|
50
|
+
resp = client.get("/v1/policy/effective", params=params)
|
|
51
|
+
else:
|
|
52
|
+
resp = client.get("/v1/policy/effective", 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
|
+
caps = data.get("capabilities") or data.get("effective") or {}
|
|
61
|
+
if isinstance(caps, list):
|
|
62
|
+
# Server returned a flat allow list.
|
|
63
|
+
from rich.table import Table
|
|
64
|
+
|
|
65
|
+
table = Table(title="[header]Effective capabilities[/header]", border_style="cyan")
|
|
66
|
+
table.add_column("Capability", style="bold")
|
|
67
|
+
for c in caps:
|
|
68
|
+
table.add_row(escape(str(c)))
|
|
69
|
+
console.print(table)
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
from rich.table import Table
|
|
73
|
+
|
|
74
|
+
table = Table(title="[header]Effective policy[/header]", border_style="cyan")
|
|
75
|
+
table.add_column("Capability", style="bold")
|
|
76
|
+
table.add_column("Decision")
|
|
77
|
+
table.add_column("Decided by", style="dim")
|
|
78
|
+
for cap, detail in caps.items():
|
|
79
|
+
decision = detail.get("decision") if isinstance(detail, dict) else str(detail)
|
|
80
|
+
tier = detail.get("decided_by_tier", "") if isinstance(detail, dict) else ""
|
|
81
|
+
style = "green" if decision == "allow" else "red"
|
|
82
|
+
table.add_row(
|
|
83
|
+
escape(str(cap)),
|
|
84
|
+
f"[{style}]{escape(str(decision))}[/{style}]",
|
|
85
|
+
escape(str(tier)),
|
|
86
|
+
)
|
|
87
|
+
console.print(table)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@policy_group.command("deployment")
|
|
91
|
+
@click.pass_context
|
|
92
|
+
@handle_errors
|
|
93
|
+
def policy_deployment(ctx: click.Context) -> None:
|
|
94
|
+
"""GET /v1/policy/deployment — show this deployment's preset + floor."""
|
|
95
|
+
cfg = ctx.obj
|
|
96
|
+
with get_client(cfg) as client:
|
|
97
|
+
resp = client.get("/v1/policy/deployment")
|
|
98
|
+
resp.raise_for_status()
|
|
99
|
+
print_json(resp.json())
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""cortexdb recall — POST /v1/recall."""
|
|
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 is_json_mode, print_recall, spinner
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command("recall")
|
|
15
|
+
@click.argument("query")
|
|
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
|
+
help="Pack shape — 'holistic' merges layers; 'granular' returns single-scope events.",
|
|
23
|
+
)
|
|
24
|
+
@click.option(
|
|
25
|
+
"--include",
|
|
26
|
+
multiple=True,
|
|
27
|
+
type=click.Choice(["events", "episodes", "facts", "beliefs", "understanding"]),
|
|
28
|
+
help="Layers to fill (repeatable). Default: facts + beliefs + episodes.",
|
|
29
|
+
)
|
|
30
|
+
@click.option(
|
|
31
|
+
"--temporal",
|
|
32
|
+
default=None,
|
|
33
|
+
help='Natural-language time window, e.g. "last 30 days".',
|
|
34
|
+
)
|
|
35
|
+
@click.option(
|
|
36
|
+
"--max-tokens",
|
|
37
|
+
type=int,
|
|
38
|
+
default=None,
|
|
39
|
+
help="Budget for the returned context_block (string).",
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--diagnostics",
|
|
43
|
+
type=click.Choice(["none", "summary", "full"]),
|
|
44
|
+
default="none",
|
|
45
|
+
show_default=True,
|
|
46
|
+
help='"none" works on every tier. "summary"/"full" need the diagnostics.read capability.',
|
|
47
|
+
)
|
|
48
|
+
@click.pass_context
|
|
49
|
+
@handle_errors
|
|
50
|
+
def recall_cmd(
|
|
51
|
+
ctx: click.Context,
|
|
52
|
+
query: str,
|
|
53
|
+
scope: str | None,
|
|
54
|
+
view: str,
|
|
55
|
+
include: tuple[str, ...],
|
|
56
|
+
temporal: str | None,
|
|
57
|
+
max_tokens: int | None,
|
|
58
|
+
diagnostics: str,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Recall context from memory.
|
|
61
|
+
|
|
62
|
+
\b
|
|
63
|
+
cortexdb recall "what did we decide about Acme?"
|
|
64
|
+
cortexdb recall "renewal status" --temporal "last 30 days"
|
|
65
|
+
"""
|
|
66
|
+
cfg = ctx.obj
|
|
67
|
+
target_scope = scope or cfg.get("scope") or cfg.get("actor")
|
|
68
|
+
if not target_scope:
|
|
69
|
+
raise click.UsageError(
|
|
70
|
+
"No scope configured. Run `cortexdb init` first, or pass --scope explicitly."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
body: dict[str, Any] = {
|
|
74
|
+
"scope": target_scope,
|
|
75
|
+
"view": view,
|
|
76
|
+
"query": query,
|
|
77
|
+
"diagnostics": diagnostics,
|
|
78
|
+
}
|
|
79
|
+
if include:
|
|
80
|
+
body["include"] = list(include)
|
|
81
|
+
if temporal:
|
|
82
|
+
body["temporal"] = {"natural": temporal}
|
|
83
|
+
if max_tokens is not None:
|
|
84
|
+
body["budgets"] = {"max_tokens": max_tokens}
|
|
85
|
+
|
|
86
|
+
with get_client(cfg) as client:
|
|
87
|
+
if not is_json_mode(cfg):
|
|
88
|
+
with spinner("Recalling..."):
|
|
89
|
+
resp = client.post("/v1/recall", json=body)
|
|
90
|
+
else:
|
|
91
|
+
resp = client.post("/v1/recall", json=body)
|
|
92
|
+
resp.raise_for_status()
|
|
93
|
+
print_recall(resp.json(), cfg)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""cortexdb scopes — /v1/scopes* (list, register, delete, members)."""
|
|
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("scopes")
|
|
19
|
+
def scopes_group() -> None:
|
|
20
|
+
"""Hierarchical scopes — list, register, members."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@scopes_group.command("list")
|
|
24
|
+
@click.option("--prefix", help="Only show scopes under this prefix")
|
|
25
|
+
@click.option("--limit", "-l", default=100, show_default=True)
|
|
26
|
+
@click.pass_context
|
|
27
|
+
@handle_errors
|
|
28
|
+
def scopes_list(ctx: click.Context, prefix: str | None, limit: int) -> None:
|
|
29
|
+
"""List scopes visible to the caller."""
|
|
30
|
+
cfg = ctx.obj
|
|
31
|
+
params: dict[str, str | int] = {"limit": limit}
|
|
32
|
+
if prefix:
|
|
33
|
+
params["prefix"] = prefix
|
|
34
|
+
with get_client(cfg) as client:
|
|
35
|
+
if not is_json_mode(cfg):
|
|
36
|
+
with spinner("Loading scopes..."):
|
|
37
|
+
resp = client.get("/v1/scopes/list", params=params)
|
|
38
|
+
else:
|
|
39
|
+
resp = client.get("/v1/scopes/list", params=params)
|
|
40
|
+
resp.raise_for_status()
|
|
41
|
+
|
|
42
|
+
data = resp.json()
|
|
43
|
+
if is_json_mode(cfg):
|
|
44
|
+
print_json(data)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
items = data.get("items", data) if isinstance(data, dict) else data
|
|
48
|
+
if not items:
|
|
49
|
+
console.print("[dim]No scopes visible.[/dim]")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
from rich.table import Table
|
|
53
|
+
|
|
54
|
+
table = Table(title="[header]Scopes[/header]", border_style="blue")
|
|
55
|
+
table.add_column("Path", style="bold")
|
|
56
|
+
table.add_column("Owner", style="cyan")
|
|
57
|
+
table.add_column("Members", justify="right", width=8)
|
|
58
|
+
for s in items:
|
|
59
|
+
members = s.get("member_count") or len(s.get("members") or [])
|
|
60
|
+
table.add_row(
|
|
61
|
+
escape(str(s.get("path") or s.get("scope", ""))),
|
|
62
|
+
escape(str(s.get("owner", ""))),
|
|
63
|
+
str(members),
|
|
64
|
+
)
|
|
65
|
+
console.print(table)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@scopes_group.command("register")
|
|
69
|
+
@click.argument("path")
|
|
70
|
+
@click.option("--owner", help="Owner actor — defaults to your actor.")
|
|
71
|
+
@click.pass_context
|
|
72
|
+
@handle_errors
|
|
73
|
+
def scopes_register(ctx: click.Context, path: str, owner: str | None) -> None:
|
|
74
|
+
"""POST /v1/scopes — register a new scope path."""
|
|
75
|
+
cfg = ctx.obj
|
|
76
|
+
body: dict[str, str] = {"path": path}
|
|
77
|
+
if owner or cfg.get("actor"):
|
|
78
|
+
body["owner"] = owner or cfg["actor"]
|
|
79
|
+
with get_client(cfg) as client:
|
|
80
|
+
resp = client.post("/v1/scopes", json=body)
|
|
81
|
+
resp.raise_for_status()
|
|
82
|
+
if is_json_mode(cfg):
|
|
83
|
+
print_json(resp.json())
|
|
84
|
+
else:
|
|
85
|
+
console.print(f"[success]Registered[/success] {path}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@scopes_group.command("members")
|
|
89
|
+
@click.argument("path")
|
|
90
|
+
@click.pass_context
|
|
91
|
+
@handle_errors
|
|
92
|
+
def scopes_members(ctx: click.Context, path: str) -> None:
|
|
93
|
+
"""GET /v1/scopes — list members of a scope."""
|
|
94
|
+
cfg = ctx.obj
|
|
95
|
+
with get_client(cfg) as client:
|
|
96
|
+
resp = client.get("/v1/scopes", params={"path": path})
|
|
97
|
+
resp.raise_for_status()
|
|
98
|
+
print_json(resp.json())
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""cortexdb search — POST /v1/recall (granular view) with optional filters."""
|
|
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 is_json_mode, print_search, spinner
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command("search")
|
|
15
|
+
@click.argument("query")
|
|
16
|
+
@click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
|
|
17
|
+
@click.option("--limit", "-l", default=10, show_default=True, help="Max results")
|
|
18
|
+
@click.option("--temporal", "-t", help='Time window, e.g. "last 7 days"')
|
|
19
|
+
@click.option(
|
|
20
|
+
"--diagnostics",
|
|
21
|
+
type=click.Choice(["none", "summary", "full"]),
|
|
22
|
+
default="none",
|
|
23
|
+
show_default=True,
|
|
24
|
+
)
|
|
25
|
+
@click.pass_context
|
|
26
|
+
@handle_errors
|
|
27
|
+
def search_cmd(
|
|
28
|
+
ctx: click.Context,
|
|
29
|
+
query: str,
|
|
30
|
+
scope: str | None,
|
|
31
|
+
limit: int,
|
|
32
|
+
temporal: str | None,
|
|
33
|
+
diagnostics: str,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Search memories — granular view, individual event hits.
|
|
36
|
+
|
|
37
|
+
Equivalent to `cortexdb recall --view granular`, optimised for
|
|
38
|
+
"show me the matching events" rather than "give me a context block."
|
|
39
|
+
"""
|
|
40
|
+
cfg = ctx.obj
|
|
41
|
+
target_scope = scope or cfg.get("scope") or cfg.get("actor")
|
|
42
|
+
if not target_scope:
|
|
43
|
+
raise click.UsageError(
|
|
44
|
+
"No scope configured. Run `cortexdb init` first, or pass --scope explicitly."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
body: dict[str, Any] = {
|
|
48
|
+
"scope": target_scope,
|
|
49
|
+
"view": "granular",
|
|
50
|
+
"query": query,
|
|
51
|
+
"diagnostics": diagnostics,
|
|
52
|
+
"budgets": {"max_events": limit},
|
|
53
|
+
}
|
|
54
|
+
if temporal:
|
|
55
|
+
body["temporal"] = {"natural": temporal}
|
|
56
|
+
|
|
57
|
+
with get_client(cfg) as client:
|
|
58
|
+
if not is_json_mode(cfg):
|
|
59
|
+
with spinner("Searching..."):
|
|
60
|
+
resp = client.post("/v1/recall", json=body)
|
|
61
|
+
else:
|
|
62
|
+
resp = client.post("/v1/recall", json=body)
|
|
63
|
+
resp.raise_for_status()
|
|
64
|
+
print_search(resp.json(), cfg)
|