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.
Files changed (36) hide show
  1. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/.gitignore +12 -0
  2. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/PKG-INFO +1 -1
  3. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/__init__.py +3 -3
  4. cortexdb_cli-0.4.2/cortexdb_cli/commands/audit.py +78 -0
  5. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/beliefs.py +70 -0
  6. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/experience.py +14 -1
  7. cortexdb_cli-0.4.2/cortexdb_cli/commands/forget.py +132 -0
  8. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/import_cmd.py +55 -1
  9. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/init.py +38 -4
  10. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/recall.py +121 -121
  11. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/main.py +6 -1
  12. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/pyproject.toml +56 -56
  13. cortexdb_cli-0.4.0/cortexdb_cli/commands/forget.py +0 -86
  14. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/README.md +0 -0
  15. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/client.py +0 -0
  16. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/__init__.py +0 -0
  17. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/admin.py +0 -0
  18. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/answer.py +0 -0
  19. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/auth.py +0 -0
  20. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/claims.py +0 -0
  21. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/compose.py +0 -0
  22. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/config_cmd.py +0 -0
  23. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/conflicts.py +0 -0
  24. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/entities.py +0 -0
  25. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/episodes.py +0 -0
  26. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/export_cmd.py +0 -0
  27. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/facts.py +0 -0
  28. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/interactive.py +0 -0
  29. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/policy.py +0 -0
  30. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/scopes.py +0 -0
  31. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/search.py +0 -0
  32. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/commands/understanding.py +0 -0
  33. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/config.py +0 -0
  34. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/errors.py +0 -0
  35. {cortexdb_cli-0.4.0 → cortexdb_cli-0.4.2}/cortexdb_cli/output.py +0 -0
  36. {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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cortexdb-cli
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Command-line interface for CortexDB — the long-term memory layer for AI.
5
5
  Project-URL: Homepage, https://cortexdb.ai
6
6
  Project-URL: Documentation, https://cortexdb.ai/docs
@@ -1,3 +1,3 @@
1
- """CortexDB CLI — command-line interface for the v1 memory API."""
2
-
3
- __version__ = "0.4.0"
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="strict_temporalfail the batch if observed_at is non-monotonic per actor.",
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=DEFAULT_ENDPOINT,
18
- show_default=True,
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
- endpoint: str,
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.0"
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