cortexdb-cli 0.4.0__tar.gz → 0.4.2__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.
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/.gitignore +12 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/PKG-INFO +1 -1
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/__init__.py +3 -3
- cortexdb_cli-0.4.2/cortexdb_cli/commands/audit.py +78 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/beliefs.py +70 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/experience.py +14 -1
- cortexdb_cli-0.4.2/cortexdb_cli/commands/forget.py +132 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/import_cmd.py +55 -1
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/init.py +38 -4
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/recall.py +121 -121
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/main.py +6 -1
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/pyproject.toml +56 -56
- cortexdb_cli-0.4.0/cortexdb_cli/commands/forget.py +0 -86
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/README.md +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/client.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/__init__.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/admin.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/answer.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/auth.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/claims.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/compose.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/config_cmd.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/conflicts.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/entities.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/episodes.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/export_cmd.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/facts.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/interactive.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/policy.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/scopes.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/search.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/understanding.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/config.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/errors.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/output.py +0 -0
- {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/state.py +0 -0
|
@@ -80,3 +80,15 @@ local-instance/
|
|
|
80
80
|
|
|
81
81
|
# doc-claims verifier scratch output
|
|
82
82
|
tools/verifier_out*/
|
|
83
|
+
|
|
84
|
+
# Generated benchmark / runtime data (audit M-10: hundreds of untracked
|
|
85
|
+
# data dirs at the workspace root slow every git status/search and risk
|
|
86
|
+
# being packaged; results JSONs are artifacts, never source)
|
|
87
|
+
benchmarks/data_*/
|
|
88
|
+
benchmarks/**/fast_results/
|
|
89
|
+
data_*/
|
|
90
|
+
*_data/
|
|
91
|
+
cortexdb_data*/
|
|
92
|
+
server_results/
|
|
93
|
+
*.log
|
|
94
|
+
docker_build.log
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"""CortexDB CLI — command-line interface for the v1 memory API."""
|
|
2
|
-
|
|
3
|
-
__version__ = "0.4.
|
|
1
|
+
"""CortexDB CLI — command-line interface for the v1 memory API."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.4.2"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""cortexdb audit — GET /v1/audit/chain + POST /v1/audit/chain/verify."""
|
|
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 console, escape, is_json_mode, print_json, spinner
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group("audit")
|
|
13
|
+
def audit_group() -> None:
|
|
14
|
+
"""Audit log integrity — hash-chain status and verification."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@audit_group.command("chain")
|
|
18
|
+
@click.option("--actor", help="Restrict chain stats to one actor's rows.")
|
|
19
|
+
@click.pass_context
|
|
20
|
+
@handle_errors
|
|
21
|
+
def audit_chain(ctx: click.Context, actor: str | None) -> None:
|
|
22
|
+
"""Show the audit hash-chain head + length (GET /v1/audit/chain).
|
|
23
|
+
|
|
24
|
+
Integrity metadata only — hashes and counts, never row content.
|
|
25
|
+
"""
|
|
26
|
+
cfg = ctx.obj
|
|
27
|
+
params: dict[str, str] = {}
|
|
28
|
+
if actor:
|
|
29
|
+
params["actor"] = actor
|
|
30
|
+
with get_client(cfg) as client:
|
|
31
|
+
resp = client.get("/v1/audit/chain", params=params or None)
|
|
32
|
+
resp.raise_for_status()
|
|
33
|
+
data = resp.json()
|
|
34
|
+
|
|
35
|
+
if is_json_mode(cfg):
|
|
36
|
+
print_json(data)
|
|
37
|
+
return
|
|
38
|
+
console.print(f"Chain length: [bold]{data.get('length', 0)}[/bold] row(s)")
|
|
39
|
+
console.print(f"Head hash: [dim]{escape(str(data.get('head_hash') or '(empty)'))}[/dim]")
|
|
40
|
+
legacy = data.get("legacy_rows", 0)
|
|
41
|
+
if legacy:
|
|
42
|
+
console.print(f"[dim]{legacy} legacy row(s) predate the chain.[/dim]")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@audit_group.command("verify")
|
|
46
|
+
@click.pass_context
|
|
47
|
+
@handle_errors
|
|
48
|
+
def audit_verify(ctx: click.Context) -> None:
|
|
49
|
+
"""Walk the whole audit chain, recomputing every row hash
|
|
50
|
+
(POST /v1/audit/chain/verify)."""
|
|
51
|
+
cfg = ctx.obj
|
|
52
|
+
with get_client(cfg) as client:
|
|
53
|
+
if not is_json_mode(cfg):
|
|
54
|
+
with spinner("Verifying chain..."):
|
|
55
|
+
resp = client.post("/v1/audit/chain/verify", json={})
|
|
56
|
+
else:
|
|
57
|
+
resp = client.post("/v1/audit/chain/verify", json={})
|
|
58
|
+
resp.raise_for_status()
|
|
59
|
+
data = resp.json()
|
|
60
|
+
|
|
61
|
+
if is_json_mode(cfg):
|
|
62
|
+
print_json(data)
|
|
63
|
+
return
|
|
64
|
+
if data.get("valid"):
|
|
65
|
+
console.print(
|
|
66
|
+
f"[success]Chain valid[/success] — {data.get('length', 0)} row(s), "
|
|
67
|
+
f"head [dim]{escape(str(data.get('head_hash') or '(empty)'))}[/dim]"
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
console.print(
|
|
71
|
+
f"[error]Chain BROKEN[/error] at row index "
|
|
72
|
+
f"[bold]{data.get('first_broken_index')}[/bold] "
|
|
73
|
+
f"of {data.get('length', 0)}."
|
|
74
|
+
)
|
|
75
|
+
raise SystemExit(1)
|
|
76
|
+
legacy = data.get("legacy_rows", 0)
|
|
77
|
+
if legacy:
|
|
78
|
+
console.print(f"[dim]{legacy} legacy row(s) predate the chain.[/dim]")
|
|
@@ -97,6 +97,76 @@ def beliefs_why(ctx: click.Context, belief_id: str) -> None:
|
|
|
97
97
|
print_json(resp.json())
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
@beliefs_group.command("create")
|
|
101
|
+
@click.argument("statement")
|
|
102
|
+
@click.option(
|
|
103
|
+
"--confidence",
|
|
104
|
+
"-c",
|
|
105
|
+
type=click.FloatRange(0.0, 1.0),
|
|
106
|
+
required=True,
|
|
107
|
+
help="Point estimate in [0, 1].",
|
|
108
|
+
)
|
|
109
|
+
@click.option(
|
|
110
|
+
"--support",
|
|
111
|
+
"-e",
|
|
112
|
+
"supports",
|
|
113
|
+
multiple=True,
|
|
114
|
+
help="Evidence ref: an event (evt_...) or fact (fact_...) id. Repeatable.",
|
|
115
|
+
)
|
|
116
|
+
@click.option("--subject", help="Claim subject (entity name); defaults to the statement itself.")
|
|
117
|
+
@click.option("--predicate", help='Claim predicate; server defaults to "asserts".')
|
|
118
|
+
@click.option("--valid-from", help="ISO-8601 valid-time start; defaults to now.")
|
|
119
|
+
@click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
|
|
120
|
+
@click.pass_context
|
|
121
|
+
@handle_errors
|
|
122
|
+
def beliefs_create(
|
|
123
|
+
ctx: click.Context,
|
|
124
|
+
statement: str,
|
|
125
|
+
confidence: float,
|
|
126
|
+
supports: tuple[str, ...],
|
|
127
|
+
subject: str | None,
|
|
128
|
+
predicate: str | None,
|
|
129
|
+
valid_from: str | None,
|
|
130
|
+
scope: str | None,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Declare a belief directly (POST /v1/beliefs).
|
|
133
|
+
|
|
134
|
+
\b
|
|
135
|
+
cortexdb beliefs create "Acme is on the enterprise plan" \\
|
|
136
|
+
--confidence 0.9 --support evt_01HX... --subject Acme --predicate plan
|
|
137
|
+
|
|
138
|
+
Declaring a newer belief over the same (subject, predicate) closes the
|
|
139
|
+
previous one.
|
|
140
|
+
"""
|
|
141
|
+
cfg = ctx.obj
|
|
142
|
+
s = _scope_or_die(cfg, scope)
|
|
143
|
+
body: dict = {
|
|
144
|
+
"scope": s,
|
|
145
|
+
"statement": statement,
|
|
146
|
+
"confidence": confidence,
|
|
147
|
+
"supports": list(supports),
|
|
148
|
+
}
|
|
149
|
+
if subject:
|
|
150
|
+
body["subject"] = subject
|
|
151
|
+
if predicate:
|
|
152
|
+
body["predicate"] = predicate
|
|
153
|
+
if valid_from:
|
|
154
|
+
body["valid_from"] = valid_from
|
|
155
|
+
with get_client(cfg) as client:
|
|
156
|
+
resp = client.post("/v1/beliefs", json=body)
|
|
157
|
+
resp.raise_for_status()
|
|
158
|
+
data = resp.json()
|
|
159
|
+
if is_json_mode(cfg):
|
|
160
|
+
print_json(data)
|
|
161
|
+
else:
|
|
162
|
+
bid = data.get("belief_id") or data.get("id", "")
|
|
163
|
+
console.print(
|
|
164
|
+
f"[success]Belief declared[/success] "
|
|
165
|
+
f"[dim]{escape(str(bid))}[/dim] — {escape(statement)} "
|
|
166
|
+
f"({confidence:.0%})"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
100
170
|
@beliefs_group.command("build")
|
|
101
171
|
@click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
|
|
102
172
|
@click.pass_context
|
|
@@ -145,7 +145,7 @@ def experience_cmd(
|
|
|
145
145
|
type=click.Choice(["independent", "strict_temporal"]),
|
|
146
146
|
default="independent",
|
|
147
147
|
show_default=True,
|
|
148
|
-
help="
|
|
148
|
+
help="Ordering hint (informational in v0.8.0 — the server processes items in submit order regardless).",
|
|
149
149
|
)
|
|
150
150
|
@click.pass_context
|
|
151
151
|
@handle_errors
|
|
@@ -199,6 +199,19 @@ def experience_bulk(
|
|
|
199
199
|
else:
|
|
200
200
|
accepted = data.get("accepted") or data.get("count") or len(items)
|
|
201
201
|
console.print(f"[success]Accepted {accepted} envelopes[/success]")
|
|
202
|
+
# v0.8.0: a 207 Multi-Status / partial bulk response carries
|
|
203
|
+
# per-item failures. httpx does not raise on 207, so warn loudly
|
|
204
|
+
# instead of silently reporting success on a partial commit.
|
|
205
|
+
if resp.status_code == 207 or data.get("partial"):
|
|
206
|
+
failures = data.get("failures") or []
|
|
207
|
+
console.print(
|
|
208
|
+
f"[warning]Partial commit: {len(failures)} item(s) were NOT written. "
|
|
209
|
+
f"Retry only the failed items.[/warning]"
|
|
210
|
+
)
|
|
211
|
+
for f in failures[:10]:
|
|
212
|
+
console.print(
|
|
213
|
+
f" - index {f.get('index')}: {f.get('error_code')} {f.get('error','')}"
|
|
214
|
+
)
|
|
202
215
|
|
|
203
216
|
|
|
204
217
|
# Hidden alias — keep `cortexdb remember "..."` working for muscle memory.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""cortexdb forget — POST /v1/forget (+ /v1/forget/preview dry runs)."""
|
|
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, escape, is_json_mode, print_forget, print_json, spinner
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command("forget")
|
|
15
|
+
@click.option("--memory-id", "-i", multiple=True, help="Event id(s) to forget. Repeatable.")
|
|
16
|
+
@click.option("--query", "-q", help='Free-text predicate, e.g. "test data" or "deal_stage:lost"')
|
|
17
|
+
@click.option("--subject", help="Forget everything where about_subject matches this value.")
|
|
18
|
+
@click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
|
|
19
|
+
@click.option("--reason", "-r", help="Audit trail note — recorded against the forget. Required unless --preview.")
|
|
20
|
+
@click.option(
|
|
21
|
+
"--cascade",
|
|
22
|
+
type=click.Choice(["none", "derived_only", "redact_events"]),
|
|
23
|
+
default="derived_only",
|
|
24
|
+
show_default=True,
|
|
25
|
+
help="How aggressively to propagate the forget across derived layers.",
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"--preview",
|
|
29
|
+
is_flag=True,
|
|
30
|
+
help="Dry run via POST /v1/forget/preview — report per-layer counts, delete nothing.",
|
|
31
|
+
)
|
|
32
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
|
|
33
|
+
@click.pass_context
|
|
34
|
+
@handle_errors
|
|
35
|
+
def forget_cmd(
|
|
36
|
+
ctx: click.Context,
|
|
37
|
+
memory_id: tuple[str, ...],
|
|
38
|
+
query: str | None,
|
|
39
|
+
subject: str | None,
|
|
40
|
+
scope: str | None,
|
|
41
|
+
reason: str | None,
|
|
42
|
+
cascade: str,
|
|
43
|
+
preview: bool,
|
|
44
|
+
yes: bool,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Delete memories with an audit trail.
|
|
47
|
+
|
|
48
|
+
\b
|
|
49
|
+
cortexdb forget --memory-id evt_01HX... --reason "user requested deletion"
|
|
50
|
+
cortexdb forget --query "test data" --reason "cleaning up test fixtures"
|
|
51
|
+
cortexdb forget --subject "user:alice" --reason "GDPR erasure" --cascade redact_events
|
|
52
|
+
cortexdb forget --memory-id evt_01HX... --preview
|
|
53
|
+
|
|
54
|
+
--preview runs the exact same request through POST /v1/forget/preview:
|
|
55
|
+
same refusals, per-layer estimated_deleted counts, nothing deleted.
|
|
56
|
+
"""
|
|
57
|
+
cfg = ctx.obj
|
|
58
|
+
target_scope = scope or cfg.get("scope") or cfg.get("actor")
|
|
59
|
+
if not target_scope:
|
|
60
|
+
raise click.UsageError(
|
|
61
|
+
"No scope configured. Run `cortexdb init` first, or pass --scope explicitly."
|
|
62
|
+
)
|
|
63
|
+
if not preview and not reason:
|
|
64
|
+
raise click.UsageError("Missing option '--reason' / '-r' (not needed with --preview).")
|
|
65
|
+
|
|
66
|
+
selector: dict[str, Any] = {}
|
|
67
|
+
if memory_id:
|
|
68
|
+
selector["memory_ids"] = list(memory_id)
|
|
69
|
+
if query:
|
|
70
|
+
# v0.8.0: the server ForgetSelector has no free-text `query` field
|
|
71
|
+
# (only memory_ids / about_subject / about_entity / predicate).
|
|
72
|
+
# Sending `query` was silently dropped → empty selector → a
|
|
73
|
+
# confusing 422 EMPTY_SELECTOR_WITHOUT_CONFIRMATION. Map --query to
|
|
74
|
+
# the subject selector, the closest supported shape.
|
|
75
|
+
selector["about_subject"] = query
|
|
76
|
+
if subject:
|
|
77
|
+
selector["about_subject"] = subject
|
|
78
|
+
if not selector:
|
|
79
|
+
raise click.UsageError("Provide at least one of --memory-id, --query, or --subject.")
|
|
80
|
+
|
|
81
|
+
# A preview is non-destructive — never gate it behind a confirmation.
|
|
82
|
+
if not preview and not yes and not is_json_mode(cfg):
|
|
83
|
+
target = (
|
|
84
|
+
f"{len(memory_id)} memory id(s)"
|
|
85
|
+
if memory_id
|
|
86
|
+
else (f'query "{query}"' if query else f"subject {subject}")
|
|
87
|
+
)
|
|
88
|
+
click.confirm(f"Forget {target} in scope {target_scope}?", abort=True)
|
|
89
|
+
|
|
90
|
+
body: dict[str, Any] = {
|
|
91
|
+
"scope": target_scope,
|
|
92
|
+
"selector": selector,
|
|
93
|
+
"cascade": cascade,
|
|
94
|
+
}
|
|
95
|
+
if reason:
|
|
96
|
+
body["audit_note"] = reason
|
|
97
|
+
|
|
98
|
+
path = "/v1/forget/preview" if preview else "/v1/forget"
|
|
99
|
+
label = "Previewing..." if preview else "Forgetting..."
|
|
100
|
+
with get_client(cfg) as client:
|
|
101
|
+
if not is_json_mode(cfg):
|
|
102
|
+
with spinner(label):
|
|
103
|
+
resp = client.post(path, json=body)
|
|
104
|
+
else:
|
|
105
|
+
resp = client.post(path, json=body)
|
|
106
|
+
resp.raise_for_status()
|
|
107
|
+
data = resp.json()
|
|
108
|
+
if preview:
|
|
109
|
+
_print_preview(data, cfg)
|
|
110
|
+
else:
|
|
111
|
+
print_forget(data, cfg)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _print_preview(data: dict[str, Any], cfg: dict[str, Any]) -> None:
|
|
115
|
+
"""Render the /v1/forget/preview response."""
|
|
116
|
+
if is_json_mode(cfg):
|
|
117
|
+
print_json(data)
|
|
118
|
+
return
|
|
119
|
+
est = data.get("estimated_deleted") or {}
|
|
120
|
+
total = sum(v for v in est.values() if isinstance(v, int))
|
|
121
|
+
lines = [f"Would delete [bold]{total}[/bold] record(s) — nothing was deleted."]
|
|
122
|
+
per_layer = ", ".join(f"{k}: {v}" for k, v in est.items() if isinstance(v, int) and v)
|
|
123
|
+
if per_layer:
|
|
124
|
+
lines.append(f"[dim]{escape(per_layer)}[/dim]")
|
|
125
|
+
pid = data.get("preview_id", "")
|
|
126
|
+
if pid:
|
|
127
|
+
lines.append(f"[dim]Preview id: {escape(pid)} (usable as from_preview_id on /v1/erasures)[/dim]")
|
|
128
|
+
from rich.panel import Panel
|
|
129
|
+
|
|
130
|
+
console.print(
|
|
131
|
+
Panel("\n".join(lines), title="[header]Forget preview[/header]", border_style="yellow")
|
|
132
|
+
)
|
|
@@ -15,7 +15,7 @@ import click
|
|
|
15
15
|
|
|
16
16
|
from cortexdb_cli.client import get_client
|
|
17
17
|
from cortexdb_cli.errors import handle_errors
|
|
18
|
-
from cortexdb_cli.output import console, is_json_mode, print_json
|
|
18
|
+
from cortexdb_cli.output import console, is_json_mode, print_json, spinner
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def _now_iso() -> str:
|
|
@@ -49,6 +49,36 @@ def _envelope(content: str, scope: str, idem: str, extra_meta: dict[str, Any]) -
|
|
|
49
49
|
return body
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
def _server_import(cfg: dict[str, Any], scope: str, raw: str) -> None:
|
|
53
|
+
"""POST /v1/import — one-shot server-side JSONL import (202)."""
|
|
54
|
+
body = {"scope": scope, "data": raw}
|
|
55
|
+
with get_client(cfg) as client:
|
|
56
|
+
if not is_json_mode(cfg):
|
|
57
|
+
with spinner("Importing..."):
|
|
58
|
+
resp = client.post("/v1/import", json=body)
|
|
59
|
+
else:
|
|
60
|
+
resp = client.post("/v1/import", json=body)
|
|
61
|
+
resp.raise_for_status()
|
|
62
|
+
data = resp.json()
|
|
63
|
+
|
|
64
|
+
if is_json_mode(cfg):
|
|
65
|
+
print_json(data)
|
|
66
|
+
return
|
|
67
|
+
accepted = data.get("accepted", 0)
|
|
68
|
+
failed = data.get("failed", 0)
|
|
69
|
+
console.print(f"[success]Imported {accepted} row(s)[/success]", end="")
|
|
70
|
+
if failed:
|
|
71
|
+
console.print(f" [warning]({failed} failed)[/warning]")
|
|
72
|
+
first = data.get("first_error")
|
|
73
|
+
if first:
|
|
74
|
+
console.print(f"[warning]First error: {first}[/warning]")
|
|
75
|
+
else:
|
|
76
|
+
console.print()
|
|
77
|
+
status_url = data.get("status_url")
|
|
78
|
+
if status_url:
|
|
79
|
+
console.print(f"[dim]Job: {data.get('import_id', '')} — status at {status_url}[/dim]")
|
|
80
|
+
|
|
81
|
+
|
|
52
82
|
@click.command("import")
|
|
53
83
|
@click.argument("file", type=click.Path(exists=True), required=False)
|
|
54
84
|
@click.option(
|
|
@@ -63,6 +93,16 @@ def _envelope(content: str, scope: str, idem: str, extra_meta: dict[str, Any]) -
|
|
|
63
93
|
is_flag=True,
|
|
64
94
|
help="Treat a .txt input as one memory rather than one-per-line.",
|
|
65
95
|
)
|
|
96
|
+
@click.option(
|
|
97
|
+
"--server",
|
|
98
|
+
"server_side",
|
|
99
|
+
is_flag=True,
|
|
100
|
+
help=(
|
|
101
|
+
"Send the input in one request to POST /v1/import (server-side JSONL "
|
|
102
|
+
"import; canonical envelopes or /v1/export lines) instead of one "
|
|
103
|
+
"POST /v1/experience per row."
|
|
104
|
+
),
|
|
105
|
+
)
|
|
66
106
|
@click.pass_context
|
|
67
107
|
@handle_errors
|
|
68
108
|
def import_cmd(
|
|
@@ -71,6 +111,7 @@ def import_cmd(
|
|
|
71
111
|
fmt: str | None,
|
|
72
112
|
scope: str | None,
|
|
73
113
|
single: bool,
|
|
114
|
+
server_side: bool,
|
|
74
115
|
) -> None:
|
|
75
116
|
"""Import memories from a file.
|
|
76
117
|
|
|
@@ -79,6 +120,7 @@ def import_cmd(
|
|
|
79
120
|
cortexdb import data.jsonl
|
|
80
121
|
cortexdb import notes.csv
|
|
81
122
|
cortexdb import notes.txt --single
|
|
123
|
+
cortexdb import backup.jsonl --server
|
|
82
124
|
cat data.json | cortexdb import -
|
|
83
125
|
"""
|
|
84
126
|
cfg = ctx.obj
|
|
@@ -109,6 +151,18 @@ def import_cmd(
|
|
|
109
151
|
else:
|
|
110
152
|
raise click.UsageError("Provide a file path or pipe from stdin.")
|
|
111
153
|
|
|
154
|
+
# Server-side path: hand the raw JSONL to POST /v1/import in one request
|
|
155
|
+
# and let the server's import engine do the row work.
|
|
156
|
+
if server_side:
|
|
157
|
+
if fmt != "jsonl":
|
|
158
|
+
raise click.UsageError(
|
|
159
|
+
"--server requires JSONL input (canonical envelopes or "
|
|
160
|
+
"/v1/export lines); pass --format jsonl for stdin or "
|
|
161
|
+
"non-.jsonl files."
|
|
162
|
+
)
|
|
163
|
+
_server_import(cfg, target_scope, raw)
|
|
164
|
+
return
|
|
165
|
+
|
|
112
166
|
# Parse
|
|
113
167
|
memories: list[dict[str, Any]] = []
|
|
114
168
|
if fmt == "json":
|
|
@@ -11,12 +11,38 @@ from cortexdb_cli.output import console
|
|
|
11
11
|
from cortexdb_cli.state import save_state, state_path
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _inherit_group_flag(
|
|
15
|
+
ctx: click.Context,
|
|
16
|
+
name: str,
|
|
17
|
+
own_value: str | None,
|
|
18
|
+
) -> str | None:
|
|
19
|
+
"""Fall back to a flag given at the GROUP level (`cortexdb --endpoint …
|
|
20
|
+
init`) when init's own flag wasn't supplied.
|
|
21
|
+
|
|
22
|
+
Users reasonably write global flags before the subcommand; Click parses
|
|
23
|
+
them into the group's params and, before 0.4.1, init silently ignored
|
|
24
|
+
them — `cortexdb --endpoint X --api-key Y init` performed an anonymous
|
|
25
|
+
signup against the DEFAULT endpoint. Only the parent's RAW params are
|
|
26
|
+
consulted (never merged config), so a stale config.toml can't leak into
|
|
27
|
+
a fresh init. If both levels are set and disagree, init's own flag wins
|
|
28
|
+
with a warning.
|
|
29
|
+
"""
|
|
30
|
+
parent = ctx.parent
|
|
31
|
+
group_value = parent.params.get(name) if parent is not None else None
|
|
32
|
+
if own_value is not None and group_value is not None and own_value != group_value:
|
|
33
|
+
console.print(
|
|
34
|
+
f"[warning]--{name.replace('_', '-')} given both before and after "
|
|
35
|
+
f"`init` with different values; using {own_value!r}.[/warning]"
|
|
36
|
+
)
|
|
37
|
+
return own_value
|
|
38
|
+
return own_value if own_value is not None else group_value
|
|
39
|
+
|
|
40
|
+
|
|
14
41
|
@click.command("init")
|
|
15
42
|
@click.option(
|
|
16
43
|
"--endpoint",
|
|
17
|
-
default=
|
|
18
|
-
|
|
19
|
-
help="CortexDB v1 API base URL.",
|
|
44
|
+
default=None,
|
|
45
|
+
help=f"CortexDB v1 API base URL. [default: {DEFAULT_ENDPOINT}]",
|
|
20
46
|
)
|
|
21
47
|
@click.option(
|
|
22
48
|
"--api-key",
|
|
@@ -33,8 +59,10 @@ from cortexdb_cli.state import save_state, state_path
|
|
|
33
59
|
default=None,
|
|
34
60
|
help="Default scope path. Defaults to the actor.",
|
|
35
61
|
)
|
|
62
|
+
@click.pass_context
|
|
36
63
|
def init_cmd(
|
|
37
|
-
|
|
64
|
+
ctx: click.Context,
|
|
65
|
+
endpoint: str | None,
|
|
38
66
|
api_key: str | None,
|
|
39
67
|
actor: str | None,
|
|
40
68
|
scope: str | None,
|
|
@@ -45,6 +73,12 @@ def init_cmd(
|
|
|
45
73
|
Without flags: anonymous signup (free tier, 7-day token TTL).
|
|
46
74
|
With --api-key: register an existing token + actor (e.g. from your IdP).
|
|
47
75
|
"""
|
|
76
|
+
# Global-flag inheritance: `cortexdb --endpoint X init` == `cortexdb init --endpoint X`.
|
|
77
|
+
endpoint = _inherit_group_flag(ctx, "endpoint", endpoint) or DEFAULT_ENDPOINT
|
|
78
|
+
api_key = _inherit_group_flag(ctx, "api_key", api_key)
|
|
79
|
+
actor = _inherit_group_flag(ctx, "actor", actor)
|
|
80
|
+
scope = _inherit_group_flag(ctx, "scope", scope)
|
|
81
|
+
|
|
48
82
|
console.print("\n[header]CortexDB CLI setup[/header]\n")
|
|
49
83
|
|
|
50
84
|
if api_key:
|
|
@@ -1,121 +1,121 @@
|
|
|
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=None,
|
|
21
|
-
help="Pack shape. Defaults to your configured view (CORTEXDB_VIEW env or "
|
|
22
|
-
"`cortexdb config set view ...`), else 'holistic'. 'descend' = the scope + all "
|
|
23
|
-
"child scopes; 'holistic' = the scope + its parents; 'granular' = exact scope only.",
|
|
24
|
-
)
|
|
25
|
-
@click.option(
|
|
26
|
-
"--include",
|
|
27
|
-
multiple=True,
|
|
28
|
-
type=click.Choice(["events", "episodes", "facts", "beliefs", "understanding"]),
|
|
29
|
-
help="Layers to fill (repeatable). Default: facts + beliefs + episodes.",
|
|
30
|
-
)
|
|
31
|
-
@click.option(
|
|
32
|
-
"--temporal",
|
|
33
|
-
default=None,
|
|
34
|
-
help='Natural-language time window, e.g. "last 30 days".',
|
|
35
|
-
)
|
|
36
|
-
@click.option(
|
|
37
|
-
"--max-tokens",
|
|
38
|
-
type=int,
|
|
39
|
-
default=None,
|
|
40
|
-
help="Budget for the returned context_block (string).",
|
|
41
|
-
)
|
|
42
|
-
@click.option(
|
|
43
|
-
"--diagnostics",
|
|
44
|
-
type=click.Choice(["none", "summary", "full"]),
|
|
45
|
-
default="none",
|
|
46
|
-
show_default=True,
|
|
47
|
-
help='"none" works on every tier. "summary"/"full" need the diagnostics.read capability.',
|
|
48
|
-
)
|
|
49
|
-
@click.option(
|
|
50
|
-
"--filter",
|
|
51
|
-
"filters",
|
|
52
|
-
multiple=True,
|
|
53
|
-
metavar="KEY=VALUE",
|
|
54
|
-
help="Hard attribute filter, repeatable (AND across keys, any-of when a "
|
|
55
|
-
"key repeats). Keys: labels, intent, modality, caller, observed_actor. "
|
|
56
|
-
"Example: --filter labels=primary_inbox",
|
|
57
|
-
)
|
|
58
|
-
@click.pass_context
|
|
59
|
-
@handle_errors
|
|
60
|
-
def recall_cmd(
|
|
61
|
-
ctx: click.Context,
|
|
62
|
-
query: str,
|
|
63
|
-
scope: str | None,
|
|
64
|
-
view: str,
|
|
65
|
-
include: tuple[str, ...],
|
|
66
|
-
temporal: str | None,
|
|
67
|
-
max_tokens: int | None,
|
|
68
|
-
diagnostics: str,
|
|
69
|
-
filters: tuple[str, ...],
|
|
70
|
-
) -> None:
|
|
71
|
-
"""Recall context from memory.
|
|
72
|
-
|
|
73
|
-
\b
|
|
74
|
-
cortexdb recall "what did we decide about Acme?"
|
|
75
|
-
cortexdb recall "renewal status" --temporal "last 30 days"
|
|
76
|
-
"""
|
|
77
|
-
cfg = ctx.obj
|
|
78
|
-
view = view or cfg.get("view") or "holistic"
|
|
79
|
-
if view not in ("raw", "granular", "structured", "holistic", "descend"):
|
|
80
|
-
raise click.UsageError(
|
|
81
|
-
f"Invalid view {view!r}. Choose: raw, granular, structured, holistic, descend."
|
|
82
|
-
)
|
|
83
|
-
target_scope = scope or cfg.get("scope") or cfg.get("actor")
|
|
84
|
-
if not target_scope:
|
|
85
|
-
raise click.UsageError(
|
|
86
|
-
"No scope configured. Run `cortexdb init` first, or pass --scope explicitly."
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
body: dict[str, Any] = {
|
|
90
|
-
"scope": target_scope,
|
|
91
|
-
"view": view,
|
|
92
|
-
"query": query,
|
|
93
|
-
"diagnostics": diagnostics,
|
|
94
|
-
}
|
|
95
|
-
if include:
|
|
96
|
-
body["include"] = list(include)
|
|
97
|
-
if temporal:
|
|
98
|
-
body["temporal"] = {"natural": temporal}
|
|
99
|
-
if max_tokens is not None:
|
|
100
|
-
body["budgets"] = {"max_tokens": max_tokens}
|
|
101
|
-
if filters:
|
|
102
|
-
fmap: dict[str, Any] = {}
|
|
103
|
-
for f in filters:
|
|
104
|
-
if "=" not in f:
|
|
105
|
-
raise click.UsageError(f"--filter must be KEY=VALUE, got {f!r}")
|
|
106
|
-
k, v = f.split("=", 1)
|
|
107
|
-
if k in fmap:
|
|
108
|
-
prev = fmap[k] if isinstance(fmap[k], list) else [fmap[k]]
|
|
109
|
-
fmap[k] = prev + [v]
|
|
110
|
-
else:
|
|
111
|
-
fmap[k] = v
|
|
112
|
-
body["filters"] = {"metadata": fmap}
|
|
113
|
-
|
|
114
|
-
with get_client(cfg) as client:
|
|
115
|
-
if not is_json_mode(cfg):
|
|
116
|
-
with spinner("Recalling..."):
|
|
117
|
-
resp = client.post("/v1/recall", json=body)
|
|
118
|
-
else:
|
|
119
|
-
resp = client.post("/v1/recall", json=body)
|
|
120
|
-
resp.raise_for_status()
|
|
121
|
-
print_recall(resp.json(), cfg)
|
|
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=None,
|
|
21
|
+
help="Pack shape. Defaults to your configured view (CORTEXDB_VIEW env or "
|
|
22
|
+
"`cortexdb config set view ...`), else 'holistic'. 'descend' = the scope + all "
|
|
23
|
+
"child scopes; 'holistic' = the scope + its parents; 'granular' = exact scope only.",
|
|
24
|
+
)
|
|
25
|
+
@click.option(
|
|
26
|
+
"--include",
|
|
27
|
+
multiple=True,
|
|
28
|
+
type=click.Choice(["events", "episodes", "facts", "beliefs", "understanding"]),
|
|
29
|
+
help="Layers to fill (repeatable). Default: facts + beliefs + episodes.",
|
|
30
|
+
)
|
|
31
|
+
@click.option(
|
|
32
|
+
"--temporal",
|
|
33
|
+
default=None,
|
|
34
|
+
help='Natural-language time window, e.g. "last 30 days".',
|
|
35
|
+
)
|
|
36
|
+
@click.option(
|
|
37
|
+
"--max-tokens",
|
|
38
|
+
type=int,
|
|
39
|
+
default=None,
|
|
40
|
+
help="Budget for the returned context_block (string).",
|
|
41
|
+
)
|
|
42
|
+
@click.option(
|
|
43
|
+
"--diagnostics",
|
|
44
|
+
type=click.Choice(["none", "summary", "full"]),
|
|
45
|
+
default="none",
|
|
46
|
+
show_default=True,
|
|
47
|
+
help='"none" works on every tier. "summary"/"full" need the diagnostics.read capability.',
|
|
48
|
+
)
|
|
49
|
+
@click.option(
|
|
50
|
+
"--filter",
|
|
51
|
+
"filters",
|
|
52
|
+
multiple=True,
|
|
53
|
+
metavar="KEY=VALUE",
|
|
54
|
+
help="Hard attribute filter, repeatable (AND across keys, any-of when a "
|
|
55
|
+
"key repeats). Keys: labels, intent, modality, caller, observed_actor. "
|
|
56
|
+
"Example: --filter labels=primary_inbox",
|
|
57
|
+
)
|
|
58
|
+
@click.pass_context
|
|
59
|
+
@handle_errors
|
|
60
|
+
def recall_cmd(
|
|
61
|
+
ctx: click.Context,
|
|
62
|
+
query: str,
|
|
63
|
+
scope: str | None,
|
|
64
|
+
view: str,
|
|
65
|
+
include: tuple[str, ...],
|
|
66
|
+
temporal: str | None,
|
|
67
|
+
max_tokens: int | None,
|
|
68
|
+
diagnostics: str,
|
|
69
|
+
filters: tuple[str, ...],
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Recall context from memory.
|
|
72
|
+
|
|
73
|
+
\b
|
|
74
|
+
cortexdb recall "what did we decide about Acme?"
|
|
75
|
+
cortexdb recall "renewal status" --temporal "last 30 days"
|
|
76
|
+
"""
|
|
77
|
+
cfg = ctx.obj
|
|
78
|
+
view = view or cfg.get("view") or "holistic"
|
|
79
|
+
if view not in ("raw", "granular", "structured", "holistic", "descend"):
|
|
80
|
+
raise click.UsageError(
|
|
81
|
+
f"Invalid view {view!r}. Choose: raw, granular, structured, holistic, descend."
|
|
82
|
+
)
|
|
83
|
+
target_scope = scope or cfg.get("scope") or cfg.get("actor")
|
|
84
|
+
if not target_scope:
|
|
85
|
+
raise click.UsageError(
|
|
86
|
+
"No scope configured. Run `cortexdb init` first, or pass --scope explicitly."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
body: dict[str, Any] = {
|
|
90
|
+
"scope": target_scope,
|
|
91
|
+
"view": view,
|
|
92
|
+
"query": query,
|
|
93
|
+
"diagnostics": diagnostics,
|
|
94
|
+
}
|
|
95
|
+
if include:
|
|
96
|
+
body["include"] = list(include)
|
|
97
|
+
if temporal:
|
|
98
|
+
body["temporal"] = {"natural": temporal}
|
|
99
|
+
if max_tokens is not None:
|
|
100
|
+
body["budgets"] = {"max_tokens": max_tokens}
|
|
101
|
+
if filters:
|
|
102
|
+
fmap: dict[str, Any] = {}
|
|
103
|
+
for f in filters:
|
|
104
|
+
if "=" not in f:
|
|
105
|
+
raise click.UsageError(f"--filter must be KEY=VALUE, got {f!r}")
|
|
106
|
+
k, v = f.split("=", 1)
|
|
107
|
+
if k in fmap:
|
|
108
|
+
prev = fmap[k] if isinstance(fmap[k], list) else [fmap[k]]
|
|
109
|
+
fmap[k] = prev + [v]
|
|
110
|
+
else:
|
|
111
|
+
fmap[k] = v
|
|
112
|
+
body["filters"] = {"metadata": fmap}
|
|
113
|
+
|
|
114
|
+
with get_client(cfg) as client:
|
|
115
|
+
if not is_json_mode(cfg):
|
|
116
|
+
with spinner("Recalling..."):
|
|
117
|
+
resp = client.post("/v1/recall", json=body)
|
|
118
|
+
else:
|
|
119
|
+
resp = client.post("/v1/recall", json=body)
|
|
120
|
+
resp.raise_for_status()
|
|
121
|
+
print_recall(resp.json(), cfg)
|
|
@@ -9,10 +9,11 @@ Usage:
|
|
|
9
9
|
cortexdb answer "..." recall + LLM answer (POST /v1/answer)
|
|
10
10
|
cortexdb compose "..." markdown document from memory (POST /v1/compose)
|
|
11
11
|
cortexdb forget --memory-id ... delete with audit (POST /v1/forget)
|
|
12
|
+
cortexdb forget --preview ... dry run (POST /v1/forget/preview)
|
|
12
13
|
cortexdb episodes {list,get,delete}
|
|
13
14
|
cortexdb entities {list,get,link}
|
|
14
15
|
cortexdb facts {list,timeline}
|
|
15
|
-
cortexdb beliefs {list,why,build}
|
|
16
|
+
cortexdb beliefs {list,why,build,create}
|
|
16
17
|
cortexdb conflicts {list,show,resolve}
|
|
17
18
|
cortexdb claims {history}
|
|
18
19
|
cortexdb concepts {list,coverage,synthesize}
|
|
@@ -20,7 +21,9 @@ Usage:
|
|
|
20
21
|
cortexdb auth {whoami,signup,tokens,revoke}
|
|
21
22
|
cortexdb policy {effective,deployment}
|
|
22
23
|
cortexdb admin {health,usage,layers}
|
|
24
|
+
cortexdb audit {chain,verify}
|
|
23
25
|
cortexdb import data.json
|
|
26
|
+
cortexdb import backup.jsonl --server server-side import (POST /v1/import)
|
|
24
27
|
cortexdb export -o backup.json
|
|
25
28
|
cortexdb config {show,set}
|
|
26
29
|
cortexdb interactive REPL
|
|
@@ -98,6 +101,7 @@ def cli(
|
|
|
98
101
|
|
|
99
102
|
from cortexdb_cli.commands.admin import admin_group
|
|
100
103
|
from cortexdb_cli.commands.answer import answer_cmd
|
|
104
|
+
from cortexdb_cli.commands.audit import audit_group
|
|
101
105
|
from cortexdb_cli.commands.compose import compose_cmd
|
|
102
106
|
from cortexdb_cli.commands.auth import auth_group
|
|
103
107
|
from cortexdb_cli.commands.beliefs import beliefs_group
|
|
@@ -146,6 +150,7 @@ cli.add_command(scopes_group)
|
|
|
146
150
|
cli.add_command(auth_group)
|
|
147
151
|
cli.add_command(policy_group)
|
|
148
152
|
cli.add_command(admin_group)
|
|
153
|
+
cli.add_command(audit_group)
|
|
149
154
|
cli.add_command(import_cmd)
|
|
150
155
|
cli.add_command(export_cmd)
|
|
151
156
|
cli.add_command(config_group)
|
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
3
|
-
build-backend = "hatchling.build"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "cortexdb-cli"
|
|
7
|
-
version = "0.4.
|
|
8
|
-
description = "Command-line interface for CortexDB — the long-term memory layer for AI."
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
license = "Apache-2.0"
|
|
11
|
-
requires-python = ">=3.10"
|
|
12
|
-
authors = [
|
|
13
|
-
{ name = "CortexDB Team", email = "team@cortexdb.ai" },
|
|
14
|
-
]
|
|
15
|
-
keywords = ["ai", "memory", "cli", "cortexdb", "agents", "llm"]
|
|
16
|
-
classifiers = [
|
|
17
|
-
"Development Status :: 4 - Beta",
|
|
18
|
-
"Environment :: Console",
|
|
19
|
-
"Intended Audience :: Developers",
|
|
20
|
-
"License :: OSI Approved :: Apache Software License",
|
|
21
|
-
"Programming Language :: Python :: 3",
|
|
22
|
-
"Programming Language :: Python :: 3.10",
|
|
23
|
-
"Programming Language :: Python :: 3.11",
|
|
24
|
-
"Programming Language :: Python :: 3.12",
|
|
25
|
-
"Programming Language :: Python :: 3.13",
|
|
26
|
-
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
27
|
-
"Topic :: Database",
|
|
28
|
-
]
|
|
29
|
-
dependencies = [
|
|
30
|
-
"click>=8.1",
|
|
31
|
-
"rich>=13.0",
|
|
32
|
-
"rich-click>=1.7",
|
|
33
|
-
"httpx>=0.27",
|
|
34
|
-
"prompt-toolkit>=3.0",
|
|
35
|
-
"tomli>=2.0; python_version < '3.11'",
|
|
36
|
-
"tomli-w>=1.0",
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
[project.scripts]
|
|
40
|
-
cortexdb = "cortexdb_cli.main:cli"
|
|
41
|
-
|
|
42
|
-
[project.urls]
|
|
43
|
-
Homepage = "https://cortexdb.ai"
|
|
44
|
-
Documentation = "https://cortexdb.ai/docs"
|
|
45
|
-
Repository = "https://github.com/cortexdb/cortexdb"
|
|
46
|
-
Issues = "https://github.com/cortexdb/cortexdb/issues"
|
|
47
|
-
|
|
48
|
-
[tool.hatch.build.targets.wheel]
|
|
49
|
-
packages = ["cortexdb_cli"]
|
|
50
|
-
|
|
51
|
-
[tool.hatch.build.targets.sdist]
|
|
52
|
-
include = ["cortexdb_cli", "README.md", "LICENSE"]
|
|
53
|
-
|
|
54
|
-
[tool.ruff]
|
|
55
|
-
target-version = "py310"
|
|
56
|
-
line-length = 100
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cortexdb-cli"
|
|
7
|
+
version = "0.4.2"
|
|
8
|
+
description = "Command-line interface for CortexDB — the long-term memory layer for AI."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "CortexDB Team", email = "team@cortexdb.ai" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ai", "memory", "cli", "cortexdb", "agents", "llm"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: Apache Software License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
27
|
+
"Topic :: Database",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"click>=8.1",
|
|
31
|
+
"rich>=13.0",
|
|
32
|
+
"rich-click>=1.7",
|
|
33
|
+
"httpx>=0.27",
|
|
34
|
+
"prompt-toolkit>=3.0",
|
|
35
|
+
"tomli>=2.0; python_version < '3.11'",
|
|
36
|
+
"tomli-w>=1.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
cortexdb = "cortexdb_cli.main:cli"
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://cortexdb.ai"
|
|
44
|
+
Documentation = "https://cortexdb.ai/docs"
|
|
45
|
+
Repository = "https://github.com/cortexdb/cortexdb"
|
|
46
|
+
Issues = "https://github.com/cortexdb/cortexdb/issues"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["cortexdb_cli"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = ["cortexdb_cli", "README.md", "LICENSE"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
target-version = "py310"
|
|
56
|
+
line-length = 100
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
"""cortexdb forget — POST /v1/forget."""
|
|
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_forget, spinner
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@click.command("forget")
|
|
15
|
-
@click.option("--memory-id", "-i", multiple=True, help="Event id(s) to forget. Repeatable.")
|
|
16
|
-
@click.option("--query", "-q", help='Free-text predicate, e.g. "test data" or "deal_stage:lost"')
|
|
17
|
-
@click.option("--subject", help="Forget everything where about_subject matches this value.")
|
|
18
|
-
@click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
|
|
19
|
-
@click.option("--reason", "-r", required=True, help="Audit trail note — recorded against the forget.")
|
|
20
|
-
@click.option(
|
|
21
|
-
"--cascade",
|
|
22
|
-
type=click.Choice(["none", "derived_only", "redact_events"]),
|
|
23
|
-
default="derived_only",
|
|
24
|
-
show_default=True,
|
|
25
|
-
help="How aggressively to propagate the forget across derived layers.",
|
|
26
|
-
)
|
|
27
|
-
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
|
|
28
|
-
@click.pass_context
|
|
29
|
-
@handle_errors
|
|
30
|
-
def forget_cmd(
|
|
31
|
-
ctx: click.Context,
|
|
32
|
-
memory_id: tuple[str, ...],
|
|
33
|
-
query: str | None,
|
|
34
|
-
subject: str | None,
|
|
35
|
-
scope: str | None,
|
|
36
|
-
reason: str,
|
|
37
|
-
cascade: str,
|
|
38
|
-
yes: bool,
|
|
39
|
-
) -> None:
|
|
40
|
-
"""Delete memories with an audit trail.
|
|
41
|
-
|
|
42
|
-
\b
|
|
43
|
-
cortexdb forget --memory-id evt_01HX... --reason "user requested deletion"
|
|
44
|
-
cortexdb forget --query "test data" --reason "cleaning up test fixtures"
|
|
45
|
-
cortexdb forget --subject "user:alice" --reason "GDPR erasure" --cascade redact_events
|
|
46
|
-
"""
|
|
47
|
-
cfg = ctx.obj
|
|
48
|
-
target_scope = scope or cfg.get("scope") or cfg.get("actor")
|
|
49
|
-
if not target_scope:
|
|
50
|
-
raise click.UsageError(
|
|
51
|
-
"No scope configured. Run `cortexdb init` first, or pass --scope explicitly."
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
selector: dict[str, Any] = {}
|
|
55
|
-
if memory_id:
|
|
56
|
-
selector["memory_ids"] = list(memory_id)
|
|
57
|
-
if query:
|
|
58
|
-
selector["query"] = query
|
|
59
|
-
if subject:
|
|
60
|
-
selector["about_subject"] = subject
|
|
61
|
-
if not selector:
|
|
62
|
-
raise click.UsageError("Provide at least one of --memory-id, --query, or --subject.")
|
|
63
|
-
|
|
64
|
-
if not yes and not is_json_mode(cfg):
|
|
65
|
-
target = (
|
|
66
|
-
f"{len(memory_id)} memory id(s)"
|
|
67
|
-
if memory_id
|
|
68
|
-
else (f'query "{query}"' if query else f"subject {subject}")
|
|
69
|
-
)
|
|
70
|
-
click.confirm(f"Forget {target} in scope {target_scope}?", abort=True)
|
|
71
|
-
|
|
72
|
-
body = {
|
|
73
|
-
"scope": target_scope,
|
|
74
|
-
"selector": selector,
|
|
75
|
-
"cascade": cascade,
|
|
76
|
-
"audit_note": reason,
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
with get_client(cfg) as client:
|
|
80
|
-
if not is_json_mode(cfg):
|
|
81
|
-
with spinner("Forgetting..."):
|
|
82
|
-
resp = client.post("/v1/forget", json=body)
|
|
83
|
-
else:
|
|
84
|
-
resp = client.post("/v1/forget", json=body)
|
|
85
|
-
resp.raise_for_status()
|
|
86
|
-
print_forget(resp.json(), cfg)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|