cortexdb-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cortexdb_cli/__init__.py +3 -0
- cortexdb_cli/client.py +92 -0
- cortexdb_cli/commands/__init__.py +1 -0
- cortexdb_cli/commands/admin.py +83 -0
- cortexdb_cli/commands/answer.py +98 -0
- cortexdb_cli/commands/auth.py +128 -0
- cortexdb_cli/commands/beliefs.py +121 -0
- cortexdb_cli/commands/config_cmd.py +50 -0
- cortexdb_cli/commands/entities.py +174 -0
- cortexdb_cli/commands/episodes.py +131 -0
- cortexdb_cli/commands/experience.py +238 -0
- cortexdb_cli/commands/export_cmd.py +65 -0
- cortexdb_cli/commands/facts.py +171 -0
- cortexdb_cli/commands/forget.py +86 -0
- cortexdb_cli/commands/import_cmd.py +198 -0
- cortexdb_cli/commands/init.py +95 -0
- cortexdb_cli/commands/interactive.py +254 -0
- cortexdb_cli/commands/policy.py +99 -0
- cortexdb_cli/commands/recall.py +93 -0
- cortexdb_cli/commands/scopes.py +98 -0
- cortexdb_cli/commands/search.py +64 -0
- cortexdb_cli/commands/understanding.py +141 -0
- cortexdb_cli/config.py +113 -0
- cortexdb_cli/errors.py +96 -0
- cortexdb_cli/main.py +140 -0
- cortexdb_cli/output.py +399 -0
- cortexdb_cli/state.py +53 -0
- cortexdb_cli-0.2.0.dist-info/METADATA +122 -0
- cortexdb_cli-0.2.0.dist-info/RECORD +31 -0
- cortexdb_cli-0.2.0.dist-info/WHEEL +4 -0
- cortexdb_cli-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""cortexdb facts — GET /v1/facts + GET /v1/facts/timeline."""
|
|
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 (
|
|
12
|
+
console,
|
|
13
|
+
escape,
|
|
14
|
+
is_json_mode,
|
|
15
|
+
print_json,
|
|
16
|
+
spinner,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _render_subject(s: Any) -> str:
|
|
21
|
+
"""Subjects are `{type, id}` or `{id}` or a bare string."""
|
|
22
|
+
if isinstance(s, dict):
|
|
23
|
+
return s.get("id") or s.get("value") or str(s)
|
|
24
|
+
return str(s) if s is not None else ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _render_object(o: Any) -> str:
|
|
28
|
+
"""Objects are `{type, datatype, value}` or `{type, id}` or a bare string."""
|
|
29
|
+
if isinstance(o, dict):
|
|
30
|
+
return str(o.get("value") or o.get("id") or o)
|
|
31
|
+
return str(o) if o is not None else ""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.group("facts")
|
|
35
|
+
def facts_group() -> None:
|
|
36
|
+
"""Facts — typed subject/predicate/object triples derived from events."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _scope_or_die(cfg: dict, scope: str | None) -> str:
|
|
40
|
+
s = scope or cfg.get("scope") or cfg.get("actor")
|
|
41
|
+
if not s:
|
|
42
|
+
raise click.UsageError(
|
|
43
|
+
"No scope configured. Run `cortexdb init` first, or pass --scope explicitly."
|
|
44
|
+
)
|
|
45
|
+
return s
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@facts_group.command("list")
|
|
49
|
+
@click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
|
|
50
|
+
@click.option("--subject", help="Filter by subject")
|
|
51
|
+
@click.option("--predicate", help="Filter by predicate")
|
|
52
|
+
@click.option("--limit", "-l", default=50, show_default=True)
|
|
53
|
+
@click.pass_context
|
|
54
|
+
@handle_errors
|
|
55
|
+
def facts_list(
|
|
56
|
+
ctx: click.Context,
|
|
57
|
+
scope: str | None,
|
|
58
|
+
subject: str | None,
|
|
59
|
+
predicate: str | None,
|
|
60
|
+
limit: int,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""List facts in a scope."""
|
|
63
|
+
cfg = ctx.obj
|
|
64
|
+
s = _scope_or_die(cfg, scope)
|
|
65
|
+
params: dict[str, str | int] = {"scope": s, "limit": limit}
|
|
66
|
+
if subject:
|
|
67
|
+
params["subject"] = subject
|
|
68
|
+
if predicate:
|
|
69
|
+
params["predicate"] = predicate
|
|
70
|
+
|
|
71
|
+
with get_client(cfg) as client:
|
|
72
|
+
if not is_json_mode(cfg):
|
|
73
|
+
with spinner("Loading facts..."):
|
|
74
|
+
resp = client.get("/v1/facts", params=params)
|
|
75
|
+
else:
|
|
76
|
+
resp = client.get("/v1/facts", params=params)
|
|
77
|
+
resp.raise_for_status()
|
|
78
|
+
|
|
79
|
+
data = resp.json()
|
|
80
|
+
if is_json_mode(cfg):
|
|
81
|
+
print_json(data)
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
items = data.get("items", data) if isinstance(data, dict) else data
|
|
85
|
+
if not items:
|
|
86
|
+
console.print("[dim]No facts in this scope yet.[/dim]")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
from rich.table import Table
|
|
90
|
+
|
|
91
|
+
table = Table(title="[header]Facts[/header]", border_style="blue")
|
|
92
|
+
table.add_column("Subject", style="bold", max_width=24)
|
|
93
|
+
table.add_column("Predicate", style="magenta", max_width=20)
|
|
94
|
+
table.add_column("Object", max_width=40)
|
|
95
|
+
table.add_column("Conf", justify="right", width=6)
|
|
96
|
+
table.add_column("Valid from", style="dim", width=20)
|
|
97
|
+
for f in items:
|
|
98
|
+
table.add_row(
|
|
99
|
+
escape(_render_subject(f.get("subject"))),
|
|
100
|
+
escape(str(f.get("predicate", ""))),
|
|
101
|
+
escape(_render_object(f.get("object"))),
|
|
102
|
+
f"{(f.get('confidence') or 0):.0%}",
|
|
103
|
+
escape(str(f.get("valid_from") or f.get("observed_at", "")))[:20],
|
|
104
|
+
)
|
|
105
|
+
console.print(table)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@facts_group.command("timeline")
|
|
109
|
+
@click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
|
|
110
|
+
@click.option("--subject", required=True, help="Subject to trace")
|
|
111
|
+
@click.option("--predicate", help="Limit timeline to one predicate")
|
|
112
|
+
@click.option("--as-of", help="Bi-temporal: only show facts known as of this timestamp")
|
|
113
|
+
@click.pass_context
|
|
114
|
+
@handle_errors
|
|
115
|
+
def facts_timeline(
|
|
116
|
+
ctx: click.Context,
|
|
117
|
+
scope: str | None,
|
|
118
|
+
subject: str,
|
|
119
|
+
predicate: str | None,
|
|
120
|
+
as_of: str | None,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Show the full bi-temporal lineage of a fact.
|
|
123
|
+
|
|
124
|
+
cortexdb facts timeline --subject "opp_xyz" --predicate stage
|
|
125
|
+
"""
|
|
126
|
+
cfg = ctx.obj
|
|
127
|
+
s = _scope_or_die(cfg, scope)
|
|
128
|
+
params: dict[str, str] = {"scope": s, "subject": subject}
|
|
129
|
+
if predicate:
|
|
130
|
+
params["predicate"] = predicate
|
|
131
|
+
if as_of:
|
|
132
|
+
params["as_of"] = as_of
|
|
133
|
+
|
|
134
|
+
with get_client(cfg) as client:
|
|
135
|
+
if not is_json_mode(cfg):
|
|
136
|
+
with spinner("Loading timeline..."):
|
|
137
|
+
resp = client.get("/v1/facts/timeline", params=params)
|
|
138
|
+
else:
|
|
139
|
+
resp = client.get("/v1/facts/timeline", params=params)
|
|
140
|
+
resp.raise_for_status()
|
|
141
|
+
|
|
142
|
+
data = resp.json()
|
|
143
|
+
if is_json_mode(cfg):
|
|
144
|
+
print_json(data)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
items = data.get("items", data) if isinstance(data, dict) else data
|
|
148
|
+
if not items:
|
|
149
|
+
console.print("[dim]No fact history for this subject.[/dim]")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
from rich.table import Table
|
|
153
|
+
|
|
154
|
+
table = Table(
|
|
155
|
+
title=f"[header]Timeline[/header] [dim]{escape(subject)}[/dim]",
|
|
156
|
+
border_style="cyan",
|
|
157
|
+
)
|
|
158
|
+
table.add_column("Predicate", style="magenta")
|
|
159
|
+
table.add_column("Object")
|
|
160
|
+
table.add_column("Valid from", style="dim")
|
|
161
|
+
table.add_column("Valid to", style="dim")
|
|
162
|
+
table.add_column("Recorded at", style="dim")
|
|
163
|
+
for f in items:
|
|
164
|
+
table.add_row(
|
|
165
|
+
escape(str(f.get("predicate", ""))),
|
|
166
|
+
escape(_render_object(f.get("object"))),
|
|
167
|
+
escape(str(f.get("valid_from", ""))[:20]),
|
|
168
|
+
escape(str(f.get("valid_to") or "")[:20]),
|
|
169
|
+
escape(str(f.get("recorded_at", ""))[:20]),
|
|
170
|
+
)
|
|
171
|
+
console.print(table)
|
|
@@ -0,0 +1,86 @@
|
|
|
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)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""cortexdb import — bulk-load memories via POST /v1/experience."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import hashlib
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from cortexdb_cli.client import get_client
|
|
17
|
+
from cortexdb_cli.errors import handle_errors
|
|
18
|
+
from cortexdb_cli.output import console, is_json_mode, print_json
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _now_iso() -> str:
|
|
22
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _idem(content: str, scope: str, i: int) -> str:
|
|
26
|
+
h = hashlib.sha256(f"{scope}|{i}|{content}".encode("utf-8")).hexdigest()[:16]
|
|
27
|
+
return f"cli-import:{h}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _envelope(content: str, scope: str, idem: str, extra_meta: dict[str, Any]) -> dict[str, Any]:
|
|
31
|
+
labels = []
|
|
32
|
+
role = extra_meta.get("role", "user")
|
|
33
|
+
if "source" in extra_meta:
|
|
34
|
+
labels.append(f"source:{extra_meta['source']}")
|
|
35
|
+
if "tags" in extra_meta:
|
|
36
|
+
for t in str(extra_meta["tags"]).split(","):
|
|
37
|
+
t = t.strip()
|
|
38
|
+
if t:
|
|
39
|
+
labels.append(t)
|
|
40
|
+
body: dict[str, Any] = {
|
|
41
|
+
"scope": scope,
|
|
42
|
+
"modality": "conversation",
|
|
43
|
+
"content": {"kind": "message", "role": role, "text": content},
|
|
44
|
+
"context": {"observed_at": extra_meta.get("observed_at", _now_iso())},
|
|
45
|
+
"idempotency_key": idem,
|
|
46
|
+
}
|
|
47
|
+
if labels:
|
|
48
|
+
body["context"]["labels"] = labels
|
|
49
|
+
return body
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@click.command("import")
|
|
53
|
+
@click.argument("file", type=click.Path(exists=True), required=False)
|
|
54
|
+
@click.option(
|
|
55
|
+
"--format",
|
|
56
|
+
"fmt",
|
|
57
|
+
type=click.Choice(["json", "jsonl", "csv", "text"]),
|
|
58
|
+
help="File format. Auto-detected from extension if omitted.",
|
|
59
|
+
)
|
|
60
|
+
@click.option("--scope", "-S", help="Scope path (defaults to your default scope)")
|
|
61
|
+
@click.option(
|
|
62
|
+
"--single",
|
|
63
|
+
is_flag=True,
|
|
64
|
+
help="Treat a .txt input as one memory rather than one-per-line.",
|
|
65
|
+
)
|
|
66
|
+
@click.pass_context
|
|
67
|
+
@handle_errors
|
|
68
|
+
def import_cmd(
|
|
69
|
+
ctx: click.Context,
|
|
70
|
+
file: str | None,
|
|
71
|
+
fmt: str | None,
|
|
72
|
+
scope: str | None,
|
|
73
|
+
single: bool,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Import memories from a file.
|
|
76
|
+
|
|
77
|
+
\b
|
|
78
|
+
cortexdb import data.json
|
|
79
|
+
cortexdb import data.jsonl
|
|
80
|
+
cortexdb import notes.csv
|
|
81
|
+
cortexdb import notes.txt --single
|
|
82
|
+
cat data.json | cortexdb import -
|
|
83
|
+
"""
|
|
84
|
+
cfg = ctx.obj
|
|
85
|
+
target_scope = scope or cfg.get("scope") or cfg.get("actor")
|
|
86
|
+
if not target_scope:
|
|
87
|
+
raise click.UsageError(
|
|
88
|
+
"No scope configured. Run `cortexdb init` first, or pass --scope explicitly."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Read input
|
|
92
|
+
if file == "-" or (file is None and not sys.stdin.isatty()):
|
|
93
|
+
raw = sys.stdin.read()
|
|
94
|
+
if not fmt:
|
|
95
|
+
fmt = "json"
|
|
96
|
+
elif file:
|
|
97
|
+
path = Path(file)
|
|
98
|
+
raw = path.read_text(encoding="utf-8")
|
|
99
|
+
if not fmt:
|
|
100
|
+
ext = path.suffix.lower()
|
|
101
|
+
fmt = {
|
|
102
|
+
".json": "json",
|
|
103
|
+
".jsonl": "jsonl",
|
|
104
|
+
".ndjson": "jsonl",
|
|
105
|
+
".csv": "csv",
|
|
106
|
+
".txt": "text",
|
|
107
|
+
".md": "text",
|
|
108
|
+
}.get(ext, "json")
|
|
109
|
+
else:
|
|
110
|
+
raise click.UsageError("Provide a file path or pipe from stdin.")
|
|
111
|
+
|
|
112
|
+
# Parse
|
|
113
|
+
memories: list[dict[str, Any]] = []
|
|
114
|
+
if fmt == "json":
|
|
115
|
+
data = json.loads(raw)
|
|
116
|
+
if isinstance(data, list):
|
|
117
|
+
memories = data
|
|
118
|
+
elif isinstance(data, dict) and "memories" in data:
|
|
119
|
+
memories = data["memories"]
|
|
120
|
+
else:
|
|
121
|
+
memories = [data]
|
|
122
|
+
elif fmt == "jsonl":
|
|
123
|
+
for line in raw.splitlines():
|
|
124
|
+
line = line.strip()
|
|
125
|
+
if not line:
|
|
126
|
+
continue
|
|
127
|
+
memories.append(json.loads(line))
|
|
128
|
+
elif fmt == "csv":
|
|
129
|
+
reader = csv.DictReader(io.StringIO(raw))
|
|
130
|
+
for row in reader:
|
|
131
|
+
if "content" in row:
|
|
132
|
+
memories.append(dict(row))
|
|
133
|
+
elif fmt == "text":
|
|
134
|
+
if single:
|
|
135
|
+
memories = [{"content": raw.strip()}]
|
|
136
|
+
else:
|
|
137
|
+
for line in raw.strip().splitlines():
|
|
138
|
+
line = line.strip()
|
|
139
|
+
if line:
|
|
140
|
+
memories.append({"content": line})
|
|
141
|
+
|
|
142
|
+
if not memories:
|
|
143
|
+
raise click.UsageError("No memories found in input.")
|
|
144
|
+
|
|
145
|
+
# POST each one
|
|
146
|
+
stored = 0
|
|
147
|
+
errors = 0
|
|
148
|
+
|
|
149
|
+
with get_client(cfg) as client:
|
|
150
|
+
total = len(memories)
|
|
151
|
+
if not is_json_mode(cfg):
|
|
152
|
+
from rich.progress import (
|
|
153
|
+
Progress,
|
|
154
|
+
SpinnerColumn,
|
|
155
|
+
BarColumn,
|
|
156
|
+
TextColumn,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
with Progress(
|
|
160
|
+
SpinnerColumn(),
|
|
161
|
+
TextColumn("[progress.description]{task.description}"),
|
|
162
|
+
BarColumn(),
|
|
163
|
+
TextColumn("{task.completed}/{task.total}"),
|
|
164
|
+
console=console,
|
|
165
|
+
) as progress:
|
|
166
|
+
task = progress.add_task("Importing...", total=total)
|
|
167
|
+
for i, mem in enumerate(memories):
|
|
168
|
+
content = mem.get("content") or mem.get("text") or ""
|
|
169
|
+
if not content:
|
|
170
|
+
progress.advance(task)
|
|
171
|
+
continue
|
|
172
|
+
extra = {k: v for k, v in mem.items() if k not in ("content", "text")}
|
|
173
|
+
body = _envelope(content, target_scope, _idem(content, target_scope, i), extra)
|
|
174
|
+
resp = client.post("/v1/experience", json=body)
|
|
175
|
+
if resp.status_code < 400:
|
|
176
|
+
stored += 1
|
|
177
|
+
else:
|
|
178
|
+
errors += 1
|
|
179
|
+
progress.advance(task)
|
|
180
|
+
|
|
181
|
+
console.print(f"\n[success]Imported {stored} memories[/success]", end="")
|
|
182
|
+
if errors:
|
|
183
|
+
console.print(f" [warning]({errors} errors)[/warning]")
|
|
184
|
+
else:
|
|
185
|
+
console.print()
|
|
186
|
+
else:
|
|
187
|
+
for i, mem in enumerate(memories):
|
|
188
|
+
content = mem.get("content") or mem.get("text") or ""
|
|
189
|
+
if not content:
|
|
190
|
+
continue
|
|
191
|
+
extra = {k: v for k, v in mem.items() if k not in ("content", "text")}
|
|
192
|
+
body = _envelope(content, target_scope, _idem(content, target_scope, i), extra)
|
|
193
|
+
resp = client.post("/v1/experience", json=body)
|
|
194
|
+
if resp.status_code < 400:
|
|
195
|
+
stored += 1
|
|
196
|
+
else:
|
|
197
|
+
errors += 1
|
|
198
|
+
print_json({"imported": stored, "errors": errors})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""cortexdb init — anonymous signup + config persistence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from cortexdb_cli.client import DEFAULT_ENDPOINT, signup
|
|
9
|
+
from cortexdb_cli.config import config_path, save_config
|
|
10
|
+
from cortexdb_cli.output import console
|
|
11
|
+
from cortexdb_cli.state import save_state, state_path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command("init")
|
|
15
|
+
@click.option(
|
|
16
|
+
"--endpoint",
|
|
17
|
+
default=DEFAULT_ENDPOINT,
|
|
18
|
+
show_default=True,
|
|
19
|
+
help="CortexDB v1 API base URL.",
|
|
20
|
+
)
|
|
21
|
+
@click.option(
|
|
22
|
+
"--api-key",
|
|
23
|
+
default=None,
|
|
24
|
+
help="Existing PASETO bearer token. If set, skips anonymous signup.",
|
|
25
|
+
)
|
|
26
|
+
@click.option(
|
|
27
|
+
"--actor",
|
|
28
|
+
default=None,
|
|
29
|
+
help="X-Cortex-Actor value. Required when --api-key is set.",
|
|
30
|
+
)
|
|
31
|
+
@click.option(
|
|
32
|
+
"--scope",
|
|
33
|
+
default=None,
|
|
34
|
+
help="Default scope path. Defaults to the actor.",
|
|
35
|
+
)
|
|
36
|
+
def init_cmd(
|
|
37
|
+
endpoint: str,
|
|
38
|
+
api_key: str | None,
|
|
39
|
+
actor: str | None,
|
|
40
|
+
scope: str | None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Set up CortexDB CLI.
|
|
43
|
+
|
|
44
|
+
\b
|
|
45
|
+
Without flags: anonymous signup (free tier, 7-day token TTL).
|
|
46
|
+
With --api-key: register an existing token + actor (e.g. from your IdP).
|
|
47
|
+
"""
|
|
48
|
+
console.print("\n[header]CortexDB CLI setup[/header]\n")
|
|
49
|
+
|
|
50
|
+
if api_key:
|
|
51
|
+
if not actor:
|
|
52
|
+
raise click.UsageError("--actor is required when --api-key is supplied.")
|
|
53
|
+
# Pure config write — no signup call.
|
|
54
|
+
save_config("default", {"endpoint": endpoint})
|
|
55
|
+
save_state(
|
|
56
|
+
{
|
|
57
|
+
"token": api_key,
|
|
58
|
+
"user_id": actor,
|
|
59
|
+
"scope": scope or actor,
|
|
60
|
+
"source": "explicit",
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
console.print(f"[success]Saved token + actor to {state_path()}[/success]")
|
|
64
|
+
console.print(f"[dim]Endpoint stored in {config_path()}[/dim]\n")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# Anonymous signup path.
|
|
68
|
+
console.print(f" Endpoint: [bold]{endpoint}[/bold]")
|
|
69
|
+
console.print(" No email, no card — free tier, 7-day token TTL.\n")
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
resp = signup(endpoint)
|
|
73
|
+
except httpx.HTTPError as exc:
|
|
74
|
+
console.print(f"[error]Signup failed: {exc}[/error]")
|
|
75
|
+
raise SystemExit(2) from exc
|
|
76
|
+
|
|
77
|
+
state = {
|
|
78
|
+
"token": resp["token"],
|
|
79
|
+
"user_id": resp["user_id"],
|
|
80
|
+
"scope": resp.get("scope") or resp["user_id"],
|
|
81
|
+
"expires_at": resp.get("expires_at"),
|
|
82
|
+
"tier": resp.get("tier", "free"),
|
|
83
|
+
"source": "signup",
|
|
84
|
+
}
|
|
85
|
+
save_state(state)
|
|
86
|
+
save_config("default", {"endpoint": endpoint})
|
|
87
|
+
|
|
88
|
+
console.print(f" [success]Token minted[/success] (expires {resp.get('expires_at', '?')})")
|
|
89
|
+
console.print(f" [success]Actor:[/success] {resp['user_id']}")
|
|
90
|
+
console.print(f" [success]Default scope:[/success] {state['scope']}")
|
|
91
|
+
console.print(f"\n[dim]State written to {state_path()}[/dim]")
|
|
92
|
+
console.print(f"[dim]Endpoint stored in {config_path()}[/dim]\n")
|
|
93
|
+
console.print("Try it:\n")
|
|
94
|
+
console.print(' [bold]cortexdb remember "Q3 revenue: $2.4M"[/bold]')
|
|
95
|
+
console.print(' [bold]cortexdb recall "Q3 revenue?"[/bold]\n')
|