cortexdb-cli 0.4.1__py3-none-any.whl → 0.4.2__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 -3
- cortexdb_cli/client.py +92 -92
- cortexdb_cli/commands/admin.py +83 -83
- cortexdb_cli/commands/audit.py +78 -0
- cortexdb_cli/commands/auth.py +128 -128
- cortexdb_cli/commands/beliefs.py +191 -121
- cortexdb_cli/commands/entities.py +174 -174
- cortexdb_cli/commands/episodes.py +131 -131
- cortexdb_cli/commands/experience.py +251 -238
- cortexdb_cli/commands/export_cmd.py +65 -65
- cortexdb_cli/commands/facts.py +171 -171
- cortexdb_cli/commands/forget.py +58 -12
- cortexdb_cli/commands/import_cmd.py +252 -198
- cortexdb_cli/commands/init.py +129 -129
- cortexdb_cli/commands/interactive.py +254 -254
- cortexdb_cli/commands/policy.py +99 -99
- cortexdb_cli/commands/scopes.py +98 -98
- cortexdb_cli/commands/search.py +64 -64
- cortexdb_cli/main.py +6 -1
- cortexdb_cli/state.py +53 -53
- {cortexdb_cli-0.4.1.dist-info → cortexdb_cli-0.4.2.dist-info}/METADATA +1 -1
- cortexdb_cli-0.4.2.dist-info/RECORD +35 -0
- cortexdb_cli-0.4.1.dist-info/RECORD +0 -34
- {cortexdb_cli-0.4.1.dist-info → cortexdb_cli-0.4.2.dist-info}/WHEEL +0 -0
- {cortexdb_cli-0.4.1.dist-info → cortexdb_cli-0.4.2.dist-info}/entry_points.txt +0 -0
cortexdb_cli/__init__.py
CHANGED
|
@@ -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"
|
cortexdb_cli/client.py
CHANGED
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
"""HTTP client for the CortexDB v1 API.
|
|
2
|
-
|
|
3
|
-
Thin wrapper around httpx that reads credentials + endpoint from the CLI
|
|
4
|
-
context and stamps every request with both `Authorization: Bearer …` and
|
|
5
|
-
`X-Cortex-Actor: …`. The actor header is mandatory on v1 — missing it
|
|
6
|
-
returns 401 ACTOR_MISMATCH no matter how valid the token is.
|
|
7
|
-
|
|
8
|
-
We deliberately don't depend on the cortexdbai SDK: keeping the CLI a
|
|
9
|
-
straight HTTP client makes the install fast and lets us pin httpx
|
|
10
|
-
independently from the SDK's release cadence.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
from contextlib import contextmanager
|
|
16
|
-
from typing import Any, Generator
|
|
17
|
-
|
|
18
|
-
import httpx
|
|
19
|
-
|
|
20
|
-
from cortexdb_cli import __version__
|
|
21
|
-
|
|
22
|
-
DEFAULT_ENDPOINT = "https://api-v1.cortexdb.ai"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class CortexClient:
|
|
26
|
-
"""Synchronous HTTP client for the CortexDB v1 API."""
|
|
27
|
-
|
|
28
|
-
def __init__(
|
|
29
|
-
self,
|
|
30
|
-
endpoint: str,
|
|
31
|
-
api_key: str,
|
|
32
|
-
actor: str = "",
|
|
33
|
-
timeout: float = 30.0,
|
|
34
|
-
) -> None:
|
|
35
|
-
self.endpoint = endpoint.rstrip("/")
|
|
36
|
-
self.api_key = api_key
|
|
37
|
-
self.actor = actor
|
|
38
|
-
headers: dict[str, str] = {
|
|
39
|
-
"Content-Type": "application/json",
|
|
40
|
-
"User-Agent": f"cortexdb-cli/{__version__}",
|
|
41
|
-
}
|
|
42
|
-
if api_key:
|
|
43
|
-
headers["Authorization"] = f"Bearer {api_key}"
|
|
44
|
-
if actor:
|
|
45
|
-
headers["X-Cortex-Actor"] = actor
|
|
46
|
-
self._http = httpx.Client(
|
|
47
|
-
base_url=self.endpoint,
|
|
48
|
-
headers=headers,
|
|
49
|
-
timeout=timeout,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
def close(self) -> None:
|
|
53
|
-
self._http.close()
|
|
54
|
-
|
|
55
|
-
def get(self, path: str, params: dict[str, Any] | None = None) -> httpx.Response:
|
|
56
|
-
return self._http.get(path, params=params)
|
|
57
|
-
|
|
58
|
-
def post(
|
|
59
|
-
self,
|
|
60
|
-
path: str,
|
|
61
|
-
json: dict[str, Any] | None = None,
|
|
62
|
-
params: dict[str, Any] | None = None,
|
|
63
|
-
) -> httpx.Response:
|
|
64
|
-
return self._http.post(path, json=json, params=params)
|
|
65
|
-
|
|
66
|
-
def delete(self, path: str, params: dict[str, Any] | None = None) -> httpx.Response:
|
|
67
|
-
return self._http.delete(path, params=params)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def signup(endpoint: str = DEFAULT_ENDPOINT, timeout: float = 30.0) -> dict[str, Any]:
|
|
71
|
-
"""Call POST /v1/auth/signup and return {token, user_id, scope, expires_at, tier}.
|
|
72
|
-
|
|
73
|
-
The endpoint is public — no auth required. Free tier; 7-day TTL.
|
|
74
|
-
"""
|
|
75
|
-
with httpx.Client(timeout=timeout) as http:
|
|
76
|
-
resp = http.post(f"{endpoint.rstrip('/')}/v1/auth/signup", json={})
|
|
77
|
-
resp.raise_for_status()
|
|
78
|
-
return resp.json()
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@contextmanager
|
|
82
|
-
def get_client(ctx: dict[str, Any]) -> Generator[CortexClient, None, None]:
|
|
83
|
-
"""Create a CortexClient from CLI context."""
|
|
84
|
-
client = CortexClient(
|
|
85
|
-
endpoint=ctx.get("endpoint", DEFAULT_ENDPOINT),
|
|
86
|
-
api_key=ctx.get("api_key", ""),
|
|
87
|
-
actor=ctx.get("actor", ""),
|
|
88
|
-
)
|
|
89
|
-
try:
|
|
90
|
-
yield client
|
|
91
|
-
finally:
|
|
92
|
-
client.close()
|
|
1
|
+
"""HTTP client for the CortexDB v1 API.
|
|
2
|
+
|
|
3
|
+
Thin wrapper around httpx that reads credentials + endpoint from the CLI
|
|
4
|
+
context and stamps every request with both `Authorization: Bearer …` and
|
|
5
|
+
`X-Cortex-Actor: …`. The actor header is mandatory on v1 — missing it
|
|
6
|
+
returns 401 ACTOR_MISMATCH no matter how valid the token is.
|
|
7
|
+
|
|
8
|
+
We deliberately don't depend on the cortexdbai SDK: keeping the CLI a
|
|
9
|
+
straight HTTP client makes the install fast and lets us pin httpx
|
|
10
|
+
independently from the SDK's release cadence.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from typing import Any, Generator
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
from cortexdb_cli import __version__
|
|
21
|
+
|
|
22
|
+
DEFAULT_ENDPOINT = "https://api-v1.cortexdb.ai"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CortexClient:
|
|
26
|
+
"""Synchronous HTTP client for the CortexDB v1 API."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
endpoint: str,
|
|
31
|
+
api_key: str,
|
|
32
|
+
actor: str = "",
|
|
33
|
+
timeout: float = 30.0,
|
|
34
|
+
) -> None:
|
|
35
|
+
self.endpoint = endpoint.rstrip("/")
|
|
36
|
+
self.api_key = api_key
|
|
37
|
+
self.actor = actor
|
|
38
|
+
headers: dict[str, str] = {
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
"User-Agent": f"cortexdb-cli/{__version__}",
|
|
41
|
+
}
|
|
42
|
+
if api_key:
|
|
43
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
44
|
+
if actor:
|
|
45
|
+
headers["X-Cortex-Actor"] = actor
|
|
46
|
+
self._http = httpx.Client(
|
|
47
|
+
base_url=self.endpoint,
|
|
48
|
+
headers=headers,
|
|
49
|
+
timeout=timeout,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def close(self) -> None:
|
|
53
|
+
self._http.close()
|
|
54
|
+
|
|
55
|
+
def get(self, path: str, params: dict[str, Any] | None = None) -> httpx.Response:
|
|
56
|
+
return self._http.get(path, params=params)
|
|
57
|
+
|
|
58
|
+
def post(
|
|
59
|
+
self,
|
|
60
|
+
path: str,
|
|
61
|
+
json: dict[str, Any] | None = None,
|
|
62
|
+
params: dict[str, Any] | None = None,
|
|
63
|
+
) -> httpx.Response:
|
|
64
|
+
return self._http.post(path, json=json, params=params)
|
|
65
|
+
|
|
66
|
+
def delete(self, path: str, params: dict[str, Any] | None = None) -> httpx.Response:
|
|
67
|
+
return self._http.delete(path, params=params)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def signup(endpoint: str = DEFAULT_ENDPOINT, timeout: float = 30.0) -> dict[str, Any]:
|
|
71
|
+
"""Call POST /v1/auth/signup and return {token, user_id, scope, expires_at, tier}.
|
|
72
|
+
|
|
73
|
+
The endpoint is public — no auth required. Free tier; 7-day TTL.
|
|
74
|
+
"""
|
|
75
|
+
with httpx.Client(timeout=timeout) as http:
|
|
76
|
+
resp = http.post(f"{endpoint.rstrip('/')}/v1/auth/signup", json={})
|
|
77
|
+
resp.raise_for_status()
|
|
78
|
+
return resp.json()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@contextmanager
|
|
82
|
+
def get_client(ctx: dict[str, Any]) -> Generator[CortexClient, None, None]:
|
|
83
|
+
"""Create a CortexClient from CLI context."""
|
|
84
|
+
client = CortexClient(
|
|
85
|
+
endpoint=ctx.get("endpoint", DEFAULT_ENDPOINT),
|
|
86
|
+
api_key=ctx.get("api_key", ""),
|
|
87
|
+
actor=ctx.get("actor", ""),
|
|
88
|
+
)
|
|
89
|
+
try:
|
|
90
|
+
yield client
|
|
91
|
+
finally:
|
|
92
|
+
client.close()
|
cortexdb_cli/commands/admin.py
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
"""cortexdb admin — health, usage, layer stats."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import click
|
|
6
|
-
|
|
7
|
-
from cortexdb_cli.client import get_client
|
|
8
|
-
from cortexdb_cli.errors import handle_errors
|
|
9
|
-
from cortexdb_cli.output import (
|
|
10
|
-
is_json_mode,
|
|
11
|
-
print_health,
|
|
12
|
-
print_ontology,
|
|
13
|
-
print_usage,
|
|
14
|
-
spinner,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@click.group("admin")
|
|
19
|
-
def admin_group() -> None:
|
|
20
|
-
"""Diagnostics — reachability, identity, rate-limit, layer status."""
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@admin_group.command("health")
|
|
24
|
-
@click.pass_context
|
|
25
|
-
@handle_errors
|
|
26
|
-
def admin_health(ctx: click.Context) -> None:
|
|
27
|
-
"""Auth + reachability check (GET /v1/auth/whoami)."""
|
|
28
|
-
cfg = ctx.obj
|
|
29
|
-
with get_client(cfg) as client:
|
|
30
|
-
resp = client.get("/v1/auth/whoami")
|
|
31
|
-
resp.raise_for_status()
|
|
32
|
-
body = resp.json()
|
|
33
|
-
# Normalise into the shape print_health expects.
|
|
34
|
-
body.setdefault("status", "ok")
|
|
35
|
-
print_health(body, cfg)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@admin_group.command("usage")
|
|
39
|
-
@click.pass_context
|
|
40
|
-
@handle_errors
|
|
41
|
-
def admin_usage(ctx: click.Context) -> None:
|
|
42
|
-
"""Tier, capabilities, rate-limit headroom, token expiry."""
|
|
43
|
-
cfg = ctx.obj
|
|
44
|
-
with get_client(cfg) as client:
|
|
45
|
-
if not is_json_mode(cfg):
|
|
46
|
-
with spinner("Loading usage..."):
|
|
47
|
-
resp = client.get("/v1/auth/whoami")
|
|
48
|
-
else:
|
|
49
|
-
resp = client.get("/v1/auth/whoami")
|
|
50
|
-
resp.raise_for_status()
|
|
51
|
-
body = resp.json()
|
|
52
|
-
# Fold rate-limit + expiry response headers into the body so the
|
|
53
|
-
# output formatter can render them in one block.
|
|
54
|
-
for k in (
|
|
55
|
-
"X-RateLimit-Limit",
|
|
56
|
-
"X-RateLimit-Reset",
|
|
57
|
-
"X-RateLimit-Remaining",
|
|
58
|
-
"X-Cortex-Token-Expires-In",
|
|
59
|
-
"X-Cortex-Token-Expires-At",
|
|
60
|
-
):
|
|
61
|
-
if k in resp.headers:
|
|
62
|
-
body[k] = resp.headers[k]
|
|
63
|
-
print_usage(body, cfg)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@admin_group.command("layers")
|
|
67
|
-
@click.pass_context
|
|
68
|
-
@handle_errors
|
|
69
|
-
def admin_layers(ctx: click.Context) -> None:
|
|
70
|
-
"""Per-layer counts and synthesis lag (GET /v1/admin/layers/stats).
|
|
71
|
-
|
|
72
|
-
Stability: experimental — the endpoint is gated by the
|
|
73
|
-
`admin.layers.stats` capability and may not be present on the free tier.
|
|
74
|
-
"""
|
|
75
|
-
cfg = ctx.obj
|
|
76
|
-
with get_client(cfg) as client:
|
|
77
|
-
if not is_json_mode(cfg):
|
|
78
|
-
with spinner("Loading layer stats..."):
|
|
79
|
-
resp = client.get("/v1/admin/layers/stats")
|
|
80
|
-
else:
|
|
81
|
-
resp = client.get("/v1/admin/layers/stats")
|
|
82
|
-
resp.raise_for_status()
|
|
83
|
-
print_ontology(resp.json(), cfg)
|
|
1
|
+
"""cortexdb admin — health, usage, layer stats."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from cortexdb_cli.client import get_client
|
|
8
|
+
from cortexdb_cli.errors import handle_errors
|
|
9
|
+
from cortexdb_cli.output import (
|
|
10
|
+
is_json_mode,
|
|
11
|
+
print_health,
|
|
12
|
+
print_ontology,
|
|
13
|
+
print_usage,
|
|
14
|
+
spinner,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group("admin")
|
|
19
|
+
def admin_group() -> None:
|
|
20
|
+
"""Diagnostics — reachability, identity, rate-limit, layer status."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@admin_group.command("health")
|
|
24
|
+
@click.pass_context
|
|
25
|
+
@handle_errors
|
|
26
|
+
def admin_health(ctx: click.Context) -> None:
|
|
27
|
+
"""Auth + reachability check (GET /v1/auth/whoami)."""
|
|
28
|
+
cfg = ctx.obj
|
|
29
|
+
with get_client(cfg) as client:
|
|
30
|
+
resp = client.get("/v1/auth/whoami")
|
|
31
|
+
resp.raise_for_status()
|
|
32
|
+
body = resp.json()
|
|
33
|
+
# Normalise into the shape print_health expects.
|
|
34
|
+
body.setdefault("status", "ok")
|
|
35
|
+
print_health(body, cfg)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@admin_group.command("usage")
|
|
39
|
+
@click.pass_context
|
|
40
|
+
@handle_errors
|
|
41
|
+
def admin_usage(ctx: click.Context) -> None:
|
|
42
|
+
"""Tier, capabilities, rate-limit headroom, token expiry."""
|
|
43
|
+
cfg = ctx.obj
|
|
44
|
+
with get_client(cfg) as client:
|
|
45
|
+
if not is_json_mode(cfg):
|
|
46
|
+
with spinner("Loading usage..."):
|
|
47
|
+
resp = client.get("/v1/auth/whoami")
|
|
48
|
+
else:
|
|
49
|
+
resp = client.get("/v1/auth/whoami")
|
|
50
|
+
resp.raise_for_status()
|
|
51
|
+
body = resp.json()
|
|
52
|
+
# Fold rate-limit + expiry response headers into the body so the
|
|
53
|
+
# output formatter can render them in one block.
|
|
54
|
+
for k in (
|
|
55
|
+
"X-RateLimit-Limit",
|
|
56
|
+
"X-RateLimit-Reset",
|
|
57
|
+
"X-RateLimit-Remaining",
|
|
58
|
+
"X-Cortex-Token-Expires-In",
|
|
59
|
+
"X-Cortex-Token-Expires-At",
|
|
60
|
+
):
|
|
61
|
+
if k in resp.headers:
|
|
62
|
+
body[k] = resp.headers[k]
|
|
63
|
+
print_usage(body, cfg)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@admin_group.command("layers")
|
|
67
|
+
@click.pass_context
|
|
68
|
+
@handle_errors
|
|
69
|
+
def admin_layers(ctx: click.Context) -> None:
|
|
70
|
+
"""Per-layer counts and synthesis lag (GET /v1/admin/layers/stats).
|
|
71
|
+
|
|
72
|
+
Stability: experimental — the endpoint is gated by the
|
|
73
|
+
`admin.layers.stats` capability and may not be present on the free tier.
|
|
74
|
+
"""
|
|
75
|
+
cfg = ctx.obj
|
|
76
|
+
with get_client(cfg) as client:
|
|
77
|
+
if not is_json_mode(cfg):
|
|
78
|
+
with spinner("Loading layer stats..."):
|
|
79
|
+
resp = client.get("/v1/admin/layers/stats")
|
|
80
|
+
else:
|
|
81
|
+
resp = client.get("/v1/admin/layers/stats")
|
|
82
|
+
resp.raise_for_status()
|
|
83
|
+
print_ontology(resp.json(), cfg)
|
|
@@ -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]")
|