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
cortexdb_cli/output.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""Output formatting for CortexDB CLI.
|
|
2
|
+
|
|
3
|
+
Two modes:
|
|
4
|
+
- Rich mode (TTY): colored panels, tables, spinners
|
|
5
|
+
- JSON mode (pipe/--json): machine-readable JSON to stdout
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from typing import Any, Generator
|
|
15
|
+
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.markup import escape
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
from rich.text import Text
|
|
21
|
+
from rich.theme import Theme
|
|
22
|
+
|
|
23
|
+
THEME = Theme({
|
|
24
|
+
"info": "cyan",
|
|
25
|
+
"success": "green bold",
|
|
26
|
+
"warning": "yellow",
|
|
27
|
+
"error": "red bold",
|
|
28
|
+
"dim": "dim",
|
|
29
|
+
"score.high": "green bold",
|
|
30
|
+
"score.mid": "yellow",
|
|
31
|
+
"score.low": "red",
|
|
32
|
+
"header": "bold magenta",
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
console = Console(theme=THEME, stderr=False)
|
|
36
|
+
err_console = Console(theme=THEME, stderr=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_json_mode(ctx: dict[str, Any]) -> bool:
|
|
40
|
+
return ctx.get("json_output", False) or not sys.stdout.isatty()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def print_json(data: Any) -> None:
|
|
44
|
+
"""Print JSON to stdout (for pipe mode)."""
|
|
45
|
+
print(json.dumps(data, indent=2, default=str))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def print_error(message: str, ctx: dict[str, Any] | None = None) -> None:
|
|
49
|
+
if ctx and is_json_mode(ctx):
|
|
50
|
+
print_json({"error": message})
|
|
51
|
+
else:
|
|
52
|
+
err_console.print(f"[error]Error:[/error] {escape(message)}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def print_success(message: str, ctx: dict[str, Any] | None = None) -> None:
|
|
56
|
+
if ctx and is_json_mode(ctx):
|
|
57
|
+
return
|
|
58
|
+
console.print(f"[success]{escape(message)}[/success]")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@contextmanager
|
|
62
|
+
def spinner(message: str = "Working...") -> Generator[None, None, None]:
|
|
63
|
+
"""Show a spinner while work is in progress."""
|
|
64
|
+
with console.status(f"[info]{message}[/info]", spinner="dots"):
|
|
65
|
+
yield
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def format_confidence(score: float) -> Text:
|
|
69
|
+
"""Color-code a confidence score."""
|
|
70
|
+
text = f"{score:.1%}"
|
|
71
|
+
if score >= 0.8:
|
|
72
|
+
return Text(text, style="score.high")
|
|
73
|
+
elif score >= 0.5:
|
|
74
|
+
return Text(text, style="score.mid")
|
|
75
|
+
else:
|
|
76
|
+
return Text(text, style="score.low")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def format_timestamp(ts: str | float | int) -> str:
|
|
80
|
+
"""Format a timestamp for display."""
|
|
81
|
+
try:
|
|
82
|
+
if isinstance(ts, (int, float)):
|
|
83
|
+
# Microsecond timestamps
|
|
84
|
+
if ts > 1e15:
|
|
85
|
+
ts = ts / 1e6
|
|
86
|
+
dt = datetime.fromtimestamp(ts, tz=timezone.utc)
|
|
87
|
+
else:
|
|
88
|
+
dt = datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
|
|
89
|
+
return dt.strftime("%Y-%m-%d %H:%M")
|
|
90
|
+
except Exception:
|
|
91
|
+
return str(ts)[:16]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# High-level formatters
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def print_health(data: dict[str, Any], ctx: dict[str, Any]) -> None:
|
|
100
|
+
"""Render the /v1/auth/whoami response as a quick auth + reachability check."""
|
|
101
|
+
if is_json_mode(ctx):
|
|
102
|
+
print_json(data)
|
|
103
|
+
return
|
|
104
|
+
caller = data.get("caller", "")
|
|
105
|
+
tenant = data.get("tenant_id", "")
|
|
106
|
+
preset = data.get("deployment_preset", "")
|
|
107
|
+
token = data.get("token", {}) or {}
|
|
108
|
+
panel_text = "[green bold]OK[/green bold] — token verified, server reachable\n"
|
|
109
|
+
if caller:
|
|
110
|
+
panel_text += f"\nCaller: [bold]{escape(caller)}[/bold]"
|
|
111
|
+
if tenant:
|
|
112
|
+
panel_text += f"\nTenant: {escape(tenant)}"
|
|
113
|
+
if preset:
|
|
114
|
+
panel_text += f"\nDeployment: {escape(preset)}"
|
|
115
|
+
if token.get("jti"):
|
|
116
|
+
panel_text += f"\nToken jti: [dim]{escape(token['jti'])}[/dim]"
|
|
117
|
+
console.print(
|
|
118
|
+
Panel(panel_text, title="[header]CortexDB health[/header]", border_style="cyan")
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def print_usage(data: dict[str, Any], ctx: dict[str, Any]) -> None:
|
|
123
|
+
"""Render whoami + rate-limit + token-expiry response headers."""
|
|
124
|
+
if is_json_mode(ctx):
|
|
125
|
+
print_json(data)
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
caller = data.get("caller", "")
|
|
129
|
+
tenant = data.get("tenant_id", "")
|
|
130
|
+
caps = data.get("effective_capabilities", []) or []
|
|
131
|
+
|
|
132
|
+
table = Table(
|
|
133
|
+
title=f"[header]Identity & quota[/header] [dim]({escape(caller)})[/dim]",
|
|
134
|
+
border_style="cyan",
|
|
135
|
+
)
|
|
136
|
+
table.add_column("Field", style="bold")
|
|
137
|
+
table.add_column("Value")
|
|
138
|
+
|
|
139
|
+
if tenant:
|
|
140
|
+
table.add_row("Tenant", escape(tenant))
|
|
141
|
+
if data.get("deployment_preset"):
|
|
142
|
+
table.add_row("Deployment", escape(data["deployment_preset"]))
|
|
143
|
+
|
|
144
|
+
if data.get("X-RateLimit-Limit"):
|
|
145
|
+
table.add_row("Rate limit", escape(data["X-RateLimit-Limit"]))
|
|
146
|
+
if data.get("X-RateLimit-Reset"):
|
|
147
|
+
table.add_row("Resets at", escape(data["X-RateLimit-Reset"]))
|
|
148
|
+
if data.get("X-Cortex-Token-Expires-In"):
|
|
149
|
+
secs = int(data["X-Cortex-Token-Expires-In"])
|
|
150
|
+
days = secs // 86400
|
|
151
|
+
hrs = (secs % 86400) // 3600
|
|
152
|
+
table.add_row("Token expires in", f"{days}d {hrs}h")
|
|
153
|
+
if data.get("X-Cortex-Token-Expires-At"):
|
|
154
|
+
table.add_row("Token expires at", escape(data["X-Cortex-Token-Expires-At"]))
|
|
155
|
+
|
|
156
|
+
table.add_row("Capabilities", f"{len(caps)} granted")
|
|
157
|
+
|
|
158
|
+
console.print(table)
|
|
159
|
+
if caps:
|
|
160
|
+
console.print(
|
|
161
|
+
"[dim]" + " ".join(escape(c) for c in caps[:24]) + "[/dim]"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def print_remember(data: dict[str, Any], ctx: dict[str, Any]) -> None:
|
|
166
|
+
"""Render the POST /v1/experience response."""
|
|
167
|
+
if is_json_mode(ctx):
|
|
168
|
+
print_json(data)
|
|
169
|
+
return
|
|
170
|
+
event_id = data.get("event_id") or data.get("id") or "unknown"
|
|
171
|
+
status = data.get("status", "captured")
|
|
172
|
+
scope = data.get("scope", "")
|
|
173
|
+
panel_text = f"[success]Stored[/success] [dim]({escape(status)})[/dim]"
|
|
174
|
+
panel_text += f"\n[dim]Event: {escape(event_id)}[/dim]"
|
|
175
|
+
if scope:
|
|
176
|
+
panel_text += f"\n[dim]Scope: {escape(scope)}[/dim]"
|
|
177
|
+
console.print(
|
|
178
|
+
Panel(panel_text, title="[header]Remember[/header]", border_style="green")
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def print_recall(data: dict[str, Any], ctx: dict[str, Any]) -> None:
|
|
183
|
+
"""Render a v1 StratifiedPack from /v1/recall."""
|
|
184
|
+
if is_json_mode(ctx):
|
|
185
|
+
print_json(data)
|
|
186
|
+
return
|
|
187
|
+
context = data.get("context_block") or data.get("context") or "(empty pack)"
|
|
188
|
+
pack_id = data.get("pack_id", "")
|
|
189
|
+
view = data.get("view", "")
|
|
190
|
+
prov = data.get("provenance", {}) or {}
|
|
191
|
+
trail = prov.get("trail") or []
|
|
192
|
+
latency = sum(p.get("elapsed_ms", 0) for p in trail if isinstance(p, dict))
|
|
193
|
+
|
|
194
|
+
header = Text()
|
|
195
|
+
if view:
|
|
196
|
+
header.append(f"View: {view} ", style="dim")
|
|
197
|
+
if latency:
|
|
198
|
+
header.append(f"Latency: {latency}ms ", style="dim")
|
|
199
|
+
if pack_id:
|
|
200
|
+
header.append(f"Pack: {pack_id}", style="dim")
|
|
201
|
+
|
|
202
|
+
console.print(
|
|
203
|
+
Panel(
|
|
204
|
+
f"{escape(str(context))}\n\n{header}",
|
|
205
|
+
title="[header]Memory recall[/header]",
|
|
206
|
+
border_style="cyan",
|
|
207
|
+
padding=(1, 2),
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def print_search(data: dict[str, Any], ctx: dict[str, Any]) -> None:
|
|
213
|
+
"""Render the granular events surface of a v1 recall pack."""
|
|
214
|
+
if is_json_mode(ctx):
|
|
215
|
+
print_json(data)
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
layers = data.get("layers", {}) or {}
|
|
219
|
+
events = layers.get("events") or data.get("events") or []
|
|
220
|
+
if isinstance(events, dict):
|
|
221
|
+
events = events.get("items", [])
|
|
222
|
+
|
|
223
|
+
if not events:
|
|
224
|
+
# Some servers return granular hits inline; fall back to context_block.
|
|
225
|
+
context = data.get("context_block") or ""
|
|
226
|
+
if context:
|
|
227
|
+
console.print(
|
|
228
|
+
Panel(escape(context), title="[header]Context[/header]", border_style="cyan")
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
console.print("[dim]No results found.[/dim]")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
table = Table(
|
|
235
|
+
title=f"[dim]{len(events)} matching events[/dim]",
|
|
236
|
+
border_style="blue",
|
|
237
|
+
show_lines=True,
|
|
238
|
+
)
|
|
239
|
+
table.add_column("#", style="dim", width=3)
|
|
240
|
+
table.add_column("Content", max_width=60)
|
|
241
|
+
table.add_column("Modality", style="cyan", width=12)
|
|
242
|
+
table.add_column("Observed at", style="dim", width=20)
|
|
243
|
+
|
|
244
|
+
for i, ev in enumerate(events, 1):
|
|
245
|
+
content = ev.get("content") or {}
|
|
246
|
+
text = ""
|
|
247
|
+
if isinstance(content, dict):
|
|
248
|
+
text = content.get("text") or content.get("summary") or ""
|
|
249
|
+
elif isinstance(content, str):
|
|
250
|
+
text = content
|
|
251
|
+
modality = ev.get("modality", "")
|
|
252
|
+
ts = (ev.get("context") or {}).get("observed_at") if isinstance(ev.get("context"), dict) else ev.get("observed_at", "")
|
|
253
|
+
table.add_row(str(i), escape(str(text)[:120]), escape(modality), format_timestamp(ts or ""))
|
|
254
|
+
|
|
255
|
+
console.print(table)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def print_episodes(data: dict[str, Any], ctx: dict[str, Any]) -> None:
|
|
259
|
+
"""Render a v1 episodes response: {items: [Episode, …]}"""
|
|
260
|
+
if is_json_mode(ctx):
|
|
261
|
+
print_json(data)
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
episodes = data.get("items") or data.get("episodes") or []
|
|
265
|
+
if not episodes:
|
|
266
|
+
console.print("[dim]No episodes found.[/dim]")
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
table = Table(
|
|
270
|
+
title=f"[header]Episodes[/header] [dim]({len(episodes)})[/dim]",
|
|
271
|
+
border_style="blue",
|
|
272
|
+
)
|
|
273
|
+
table.add_column("ID", style="dim", max_width=16)
|
|
274
|
+
table.add_column("Summary", max_width=60)
|
|
275
|
+
table.add_column("Span events", style="cyan", justify="right", width=12)
|
|
276
|
+
table.add_column("Valid from", style="dim", width=20)
|
|
277
|
+
|
|
278
|
+
for ep in episodes:
|
|
279
|
+
eid = str(ep.get("episode_id") or ep.get("id", ""))[:16]
|
|
280
|
+
summary = ep.get("summary") or ep.get("title") or ""
|
|
281
|
+
spans = ep.get("event_count") or len(ep.get("event_ids") or [])
|
|
282
|
+
valid_from = ep.get("valid_from") or ep.get("observed_at", "")
|
|
283
|
+
table.add_row(
|
|
284
|
+
escape(eid), escape(str(summary)[:80]), str(spans), format_timestamp(valid_from)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
console.print(table)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def print_entities(data: list[dict[str, Any]], ctx: dict[str, Any]) -> None:
|
|
291
|
+
"""Render entity rollups derived from /v1/facts (subject, predicate count)."""
|
|
292
|
+
if is_json_mode(ctx):
|
|
293
|
+
print_json(data)
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
if not data:
|
|
297
|
+
console.print("[dim]No entities found.[/dim]")
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
table = Table(title="[header]Entities[/header]", border_style="blue")
|
|
301
|
+
table.add_column("Subject", style="bold", max_width=40)
|
|
302
|
+
table.add_column("Predicates", style="cyan", max_width=60)
|
|
303
|
+
table.add_column("Facts", justify="right")
|
|
304
|
+
|
|
305
|
+
for ent in data:
|
|
306
|
+
preds = ent.get("predicates") or []
|
|
307
|
+
preds_str = ", ".join(preds[:6])
|
|
308
|
+
if len(preds) > 6:
|
|
309
|
+
preds_str += f", +{len(preds) - 6}"
|
|
310
|
+
table.add_row(
|
|
311
|
+
escape(str(ent.get("subject") or ent.get("id", ""))),
|
|
312
|
+
escape(preds_str),
|
|
313
|
+
str(ent.get("fact_count", 0)),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
console.print(table)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def print_entity_detail(data: dict[str, Any], ctx: dict[str, Any]) -> None:
|
|
320
|
+
"""Render every fact about an entity (output of /v1/facts?subject=…)."""
|
|
321
|
+
if is_json_mode(ctx):
|
|
322
|
+
print_json(data)
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
subject = data.get("id") or data.get("subject", "")
|
|
326
|
+
facts = data.get("facts") or []
|
|
327
|
+
console.print(
|
|
328
|
+
Panel(
|
|
329
|
+
f"[bold]{escape(subject)}[/bold]\n[dim]{len(facts)} facts[/dim]",
|
|
330
|
+
title="[header]Entity[/header]",
|
|
331
|
+
border_style="cyan",
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if not facts:
|
|
336
|
+
console.print("[dim]No facts recorded yet — the LLM extractor may not have run on related events.[/dim]")
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
table = Table(border_style="blue")
|
|
340
|
+
table.add_column("Predicate", style="magenta", max_width=24)
|
|
341
|
+
table.add_column("Object", style="bold")
|
|
342
|
+
table.add_column("Confidence", justify="right", width=10)
|
|
343
|
+
table.add_column("Valid from", style="dim", width=20)
|
|
344
|
+
for f in facts:
|
|
345
|
+
obj = f.get("object")
|
|
346
|
+
if isinstance(obj, dict):
|
|
347
|
+
obj_str = str(obj.get("value") or obj.get("id") or obj)
|
|
348
|
+
else:
|
|
349
|
+
obj_str = str(obj or "")
|
|
350
|
+
table.add_row(
|
|
351
|
+
escape(f.get("predicate", "")),
|
|
352
|
+
escape(obj_str),
|
|
353
|
+
f"{(f.get('confidence') or 0):.0%}",
|
|
354
|
+
format_timestamp(f.get("valid_from") or f.get("observed_at", "")),
|
|
355
|
+
)
|
|
356
|
+
console.print(table)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def print_forget(data: dict[str, Any], ctx: dict[str, Any]) -> None:
|
|
360
|
+
"""Render the /v1/forget response."""
|
|
361
|
+
if is_json_mode(ctx):
|
|
362
|
+
print_json(data)
|
|
363
|
+
return
|
|
364
|
+
deleted = data.get("deleted_count") or data.get("forgotten") or 0
|
|
365
|
+
erasure_id = data.get("erasure_id") or data.get("audit_id", "")
|
|
366
|
+
cascade = data.get("cascade", "")
|
|
367
|
+
panel_text = f"Forgot [bold]{deleted}[/bold] item(s)"
|
|
368
|
+
if cascade:
|
|
369
|
+
panel_text += f" [dim](cascade: {escape(cascade)})[/dim]"
|
|
370
|
+
if erasure_id:
|
|
371
|
+
panel_text += f"\n[dim]Erasure id: {escape(erasure_id)}[/dim]"
|
|
372
|
+
console.print(
|
|
373
|
+
Panel(panel_text, title="[header]Forget[/header]", border_style="yellow")
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def print_ontology(data: dict[str, Any], ctx: dict[str, Any]) -> None:
|
|
378
|
+
"""Render /v1/admin/layers/stats (per-layer counts + lag)."""
|
|
379
|
+
if is_json_mode(ctx):
|
|
380
|
+
print_json(data)
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
layers = data.get("layers") or data
|
|
384
|
+
if not isinstance(layers, dict) or not layers:
|
|
385
|
+
console.print("[dim]No layer stats available on this tier.[/dim]")
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
table = Table(title="[header]Layer stats[/header]", border_style="cyan")
|
|
389
|
+
table.add_column("Layer", style="bold")
|
|
390
|
+
table.add_column("Count", justify="right")
|
|
391
|
+
table.add_column("Lag", style="dim", justify="right")
|
|
392
|
+
for name, info in layers.items():
|
|
393
|
+
if isinstance(info, dict):
|
|
394
|
+
count = info.get("count", info.get("total", ""))
|
|
395
|
+
lag = info.get("lag") or info.get("synthesis_lag", "")
|
|
396
|
+
table.add_row(escape(name), str(count), escape(str(lag)))
|
|
397
|
+
else:
|
|
398
|
+
table.add_row(escape(name), str(info), "")
|
|
399
|
+
console.print(table)
|
cortexdb_cli/state.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Persistent state for the CortexDB CLI.
|
|
2
|
+
|
|
3
|
+
We split secrets out of `~/.cortexdb/config.toml` (which is meant to be
|
|
4
|
+
human-editable) and into `~/.cortexdb/state.json`, written 0600 on POSIX.
|
|
5
|
+
State holds the PASETO bearer, actor, scope, and token expiry produced by
|
|
6
|
+
`POST /v1/auth/signup` — exactly the same shape the cortexdb-mcp 0.3.0
|
|
7
|
+
server persists. Keeping the format identical means a user who signed up
|
|
8
|
+
through one tool can copy state.json across to use the other.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import stat
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from cortexdb_cli.config import config_dir
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def state_path() -> Path:
|
|
23
|
+
return config_dir() / "state.json"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_state() -> dict[str, Any]:
|
|
27
|
+
"""Load persistent state. Returns {} if no state file exists."""
|
|
28
|
+
path = state_path()
|
|
29
|
+
if not path.exists():
|
|
30
|
+
return {}
|
|
31
|
+
try:
|
|
32
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
33
|
+
except (OSError, json.JSONDecodeError):
|
|
34
|
+
return {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def save_state(state: dict[str, Any]) -> None:
|
|
38
|
+
"""Atomically write state and tighten permissions on POSIX."""
|
|
39
|
+
path = state_path()
|
|
40
|
+
tmp = path.with_suffix(".json.tmp")
|
|
41
|
+
tmp.write_text(json.dumps(state, indent=2, sort_keys=True), encoding="utf-8")
|
|
42
|
+
tmp.replace(path)
|
|
43
|
+
if os.name == "posix":
|
|
44
|
+
try:
|
|
45
|
+
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
|
46
|
+
except OSError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def clear_state() -> None:
|
|
51
|
+
path = state_path()
|
|
52
|
+
if path.exists():
|
|
53
|
+
path.unlink()
|
|
@@ -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,31 @@
|
|
|
1
|
+
cortexdb_cli/__init__.py,sha256=ixhxihEV952X_SmT9JyodCc15IxaOntRFdA1XOFz72g,92
|
|
2
|
+
cortexdb_cli/client.py,sha256=t2i1jlIoNkppYaKtZMTkv99rwXI9v9iLQ_2BCX599Cw,2866
|
|
3
|
+
cortexdb_cli/config.py,sha256=JrHwQsfLAw56j0Yrafp_4HyW4TqIUVBEZuZFjxSlB0c,3609
|
|
4
|
+
cortexdb_cli/errors.py,sha256=xwxAWYY7KtNa-e7dyLQECnsKcciVcKC41CXFFF_-0VU,3924
|
|
5
|
+
cortexdb_cli/main.py,sha256=RLqcyQzDSTk-IlE2jJ9Camf3FrqH2GZb-TAMRItwB80,5272
|
|
6
|
+
cortexdb_cli/output.py,sha256=X2XosVQXEHPSQy0UnEzJyCDiOI9h3TRMneLlgEaEFQM,14014
|
|
7
|
+
cortexdb_cli/state.py,sha256=yxy1ODTdBkRUKnTz8vBEvojuH4ieRbME4ccVMj--bwk,1529
|
|
8
|
+
cortexdb_cli/commands/__init__.py,sha256=TeF4ieMQkbgBr9Fco9VAiIZvKQ4vdg4yhkz4fvwfToc,37
|
|
9
|
+
cortexdb_cli/commands/admin.py,sha256=1fyMVujNTBzBkWthgj1GXRsWXGM2FyBv_PfvZUMBfFQ,2516
|
|
10
|
+
cortexdb_cli/commands/answer.py,sha256=m9Tn2XAdXpwgYQDEaBVex3PAyGO5NisVAhU83dzoDVo,2904
|
|
11
|
+
cortexdb_cli/commands/auth.py,sha256=0kbOZenps3ydPM_HIisZNXFoQhNuG4hyBNnvF5UYHt8,4521
|
|
12
|
+
cortexdb_cli/commands/beliefs.py,sha256=55zNUMfwzqRWlkGHuv5xJFN_QEeLMhqXK2qf5zl9gCc,4043
|
|
13
|
+
cortexdb_cli/commands/config_cmd.py,sha256=9XwPJLWubvc6QY8EnVJd8mhQFSITnhQYlclJaQqr0VU,1519
|
|
14
|
+
cortexdb_cli/commands/entities.py,sha256=8jQwL14yzwF2j7_-447WTlFTFsNxZ30MWH4na1K2CdE,6067
|
|
15
|
+
cortexdb_cli/commands/episodes.py,sha256=oeXSSgnt_FPqgoI2nTHdYe56iwo6ydzR2Bp1_K25CV0,4135
|
|
16
|
+
cortexdb_cli/commands/experience.py,sha256=13u8907HrQz8UepD9agv22UOMzOdV6Hq654qswdsgOQ,7591
|
|
17
|
+
cortexdb_cli/commands/export_cmd.py,sha256=v5nnApGz6yJRhnTh2CCB8siYI1xJfn9dOaeSgZb1Zdo,1986
|
|
18
|
+
cortexdb_cli/commands/facts.py,sha256=2eFLum7kIG3QiS5_M_5yrv8puxwXZ-bE8dMhtks9Fco,5405
|
|
19
|
+
cortexdb_cli/commands/forget.py,sha256=tEhXHlpM6mXIxe0TJrwAnfNNBvDXm1dNgVXwKuNHpaM,2948
|
|
20
|
+
cortexdb_cli/commands/import_cmd.py,sha256=SEsxTStwS8LF5ebehCZtvyYGAPEAnq5rqFPTrem3oCY,6458
|
|
21
|
+
cortexdb_cli/commands/init.py,sha256=FgFwPell0x8-rA1ECKiT0cBcDWoIhCVS6TsRp-_JH84,3035
|
|
22
|
+
cortexdb_cli/commands/interactive.py,sha256=L0qUCMfm1jzPrPKvKXqu9Tiz9qvcrRCsICs7OAvyZ3I,9518
|
|
23
|
+
cortexdb_cli/commands/policy.py,sha256=IT2o-OogGHfcBCyMM3ht_OCpmG7d2ryHsJpFxK-l6R4,3362
|
|
24
|
+
cortexdb_cli/commands/recall.py,sha256=nmFVQyojaI6xHVWh3rIF5nKo8_chYc7BFlp_zu4JPXk,2680
|
|
25
|
+
cortexdb_cli/commands/scopes.py,sha256=6cNGt9mHJe8M10QkekifhJ86TVuzWIOxHzxUrDUQPhY,3052
|
|
26
|
+
cortexdb_cli/commands/search.py,sha256=aRAd8161ItytQJvYaG7K1an003GA8yDbW4AwOhfRkIE,1957
|
|
27
|
+
cortexdb_cli/commands/understanding.py,sha256=6YJEmRPGkP6jqn7RQgp6zkbbvYZis2eHDbQ190Y0UXM,5009
|
|
28
|
+
cortexdb_cli-0.2.0.dist-info/METADATA,sha256=lnIlhxdPYCQVaCAn-osZ3BXuLC2vyP0u3SZ29mw3R0o,4429
|
|
29
|
+
cortexdb_cli-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
30
|
+
cortexdb_cli-0.2.0.dist-info/entry_points.txt,sha256=h3sEbdqqvtv8LBbRLUgybYPo9N2lpU85fQUSh5ToEvs,51
|
|
31
|
+
cortexdb_cli-0.2.0.dist-info/RECORD,,
|