memorysync-cli 1.1.0__tar.gz → 1.1.1__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.
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/PKG-INFO +1 -1
- memorysync_cli-1.1.1/src/memorysync_cli/_version.py +1 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/commands/admin.py +164 -21
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/commands/init.py +8 -3
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/config.py +23 -3
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/http.py +11 -1
- memorysync_cli-1.1.0/src/memorysync_cli/_version.py +0 -1
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/.gitignore +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/LICENSE +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/README.md +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/pyproject.toml +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/__init__.py +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/__main__.py +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/args.py +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/commands/__init__.py +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/commands/memory.py +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/commands/source.py +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/commands/tooling.py +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/completions.py +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/credentials.py +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/errors.py +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/evaluation.py +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/main.py +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/output.py +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/registry.json +0 -0
- {memorysync_cli-1.1.0 → memorysync_cli-1.1.1}/src/memorysync_cli/registry.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.1"
|
|
@@ -20,8 +20,18 @@ from .. import config as config_module
|
|
|
20
20
|
from .. import credentials
|
|
21
21
|
from .._version import __version__
|
|
22
22
|
from ..errors import usage_error
|
|
23
|
+
from ..evaluation import is_evaluation_key
|
|
23
24
|
from ..output import render_table, style
|
|
24
25
|
|
|
26
|
+
#: Keys ``config set`` / ``config unset`` accept. Identical to the Node CLI's
|
|
27
|
+
#: ``SETTABLE``; ``output`` was previously missing here, so `config set output table`
|
|
28
|
+
#: worked under one CLI and was refused by the other.
|
|
29
|
+
#:
|
|
30
|
+
#: ``api_key`` is deliberately absent. Keys go to the keychain through ``init``, and
|
|
31
|
+
#: accepting one here would write a credential into a file people paste into bug
|
|
32
|
+
#: reports.
|
|
33
|
+
_SETTABLE = frozenset({"user", "project", "base_url", "output", "timeout"})
|
|
34
|
+
|
|
25
35
|
|
|
26
36
|
def _percent(used: Any, limit: Any) -> str:
|
|
27
37
|
if not isinstance(used, (int, float)) or not isinstance(limit, (int, float)) or limit <= 0:
|
|
@@ -29,6 +39,38 @@ def _percent(used: Any, limit: Any) -> str:
|
|
|
29
39
|
return f"{min(100, round(used / limit * 100))}%"
|
|
30
40
|
|
|
31
41
|
|
|
42
|
+
def _evaluation_usage_summary(api: Any) -> dict:
|
|
43
|
+
"""Reshape ``/evaluation/usage`` into what ``quota`` already renders.
|
|
44
|
+
|
|
45
|
+
Same keys the billing summary returns, so the renderer and ``--json`` consumers
|
|
46
|
+
do not have to care which kind of key they are looking at. Mirrors the Node
|
|
47
|
+
CLI's ``evaluationQuota``.
|
|
48
|
+
"""
|
|
49
|
+
usage = api.evaluation_usage() or {}
|
|
50
|
+
add = usage.get("add_requests") or {}
|
|
51
|
+
retrieval = usage.get("retrieval_requests") or {}
|
|
52
|
+
return {
|
|
53
|
+
"plan_id": usage.get("plan"),
|
|
54
|
+
"metrics": [
|
|
55
|
+
{
|
|
56
|
+
"metric": "add_requests",
|
|
57
|
+
"label": "Memories added",
|
|
58
|
+
"used": add.get("used"),
|
|
59
|
+
"limit": add.get("limit"),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"metric": "retrieval_requests",
|
|
63
|
+
"label": "Retrievals",
|
|
64
|
+
"used": retrieval.get("used"),
|
|
65
|
+
"limit": retrieval.get("limit"),
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
"claimed": usage.get("claimed") is True,
|
|
69
|
+
"expires_at": usage.get("expires_at"),
|
|
70
|
+
"claim_command": usage.get("claim_command"),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
32
74
|
def quota(ctx: dict) -> dict:
|
|
33
75
|
"""Plan usage, with headroom made explicit.
|
|
34
76
|
|
|
@@ -36,7 +78,17 @@ def quota(ctx: dict) -> dict:
|
|
|
36
78
|
Mem0, Supermemory or Zep expose this from their CLI.
|
|
37
79
|
"""
|
|
38
80
|
api = ctx["api"]
|
|
39
|
-
|
|
81
|
+
|
|
82
|
+
if is_evaluation_key(ctx.get("api_key")):
|
|
83
|
+
# ``/org/billing/usage-summary`` needs the ``billing.view`` capability and
|
|
84
|
+
# the ``billing:read`` scope, and an evaluation key is granted neither on
|
|
85
|
+
# purpose: ``billing:read`` also opens ``GET /org/billing/profit``, which
|
|
86
|
+
# returns cost and margin from our internal pricing model. The server
|
|
87
|
+
# exposes the same counters without the priced fields, so route there.
|
|
88
|
+
summary = _evaluation_usage_summary(api)
|
|
89
|
+
else:
|
|
90
|
+
summary = api.usage_summary() or {}
|
|
91
|
+
|
|
40
92
|
metrics = [m for m in (summary.get("metrics") or []) if isinstance(m, dict)]
|
|
41
93
|
|
|
42
94
|
rows = [
|
|
@@ -71,6 +123,15 @@ def quota(ctx: dict) -> dict:
|
|
|
71
123
|
for row in rows
|
|
72
124
|
],
|
|
73
125
|
)
|
|
126
|
+
# An evaluation allowance does not reset on a cycle: the key stops working
|
|
127
|
+
# at expiry, so that is the date worth printing. Matches the Node CLI.
|
|
128
|
+
expiry_note = ""
|
|
129
|
+
if summary.get("expires_at") and not summary.get("claimed"):
|
|
130
|
+
expiry_note = "\n" + style.yellow(
|
|
131
|
+
"Unclaimed evaluation key, expires "
|
|
132
|
+
f"{str(summary['expires_at'])[:10]}."
|
|
133
|
+
)
|
|
134
|
+
|
|
74
135
|
exhausted = [r["metric"] for r in rows if r["remaining"] == 0]
|
|
75
136
|
if exhausted:
|
|
76
137
|
warning = style.yellow(
|
|
@@ -80,8 +141,8 @@ def quota(ctx: dict) -> dict:
|
|
|
80
141
|
"Writes and reads return empty rather than failing, so check here "
|
|
81
142
|
"before assuming memory is missing."
|
|
82
143
|
)
|
|
83
|
-
return f"{table}\n\n{warning}\n{note}"
|
|
84
|
-
return table
|
|
144
|
+
return f"{table}\n\n{warning}\n{note}{expiry_note}"
|
|
145
|
+
return f"{table}{expiry_note}"
|
|
85
146
|
|
|
86
147
|
return {"data": rows, "text": render}
|
|
87
148
|
|
|
@@ -323,41 +384,123 @@ def config_command(ctx: dict) -> dict:
|
|
|
323
384
|
),
|
|
324
385
|
}
|
|
325
386
|
|
|
387
|
+
if sub == "profiles":
|
|
388
|
+
cfg = config_module.load()
|
|
389
|
+
current = cfg.get("current_profile", "default")
|
|
390
|
+
profiles = cfg.get("profiles", {})
|
|
391
|
+
rows = [
|
|
392
|
+
[
|
|
393
|
+
style.green("*") if name == current else " ",
|
|
394
|
+
name,
|
|
395
|
+
entry.get("user") or "-",
|
|
396
|
+
entry.get("project") or "-",
|
|
397
|
+
entry.get("base_url") or "(default)",
|
|
398
|
+
]
|
|
399
|
+
for name, entry in profiles.items()
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
def render_profiles() -> str:
|
|
403
|
+
if not rows:
|
|
404
|
+
return style.dim("No profiles. Run `memorysync init`.")
|
|
405
|
+
return render_table([" ", "profile", "user", "project", "base url"], rows)
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
"data": {"current": current, "profiles": profiles},
|
|
409
|
+
"text": render_profiles,
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if sub == "use-profile":
|
|
413
|
+
if len(positionals) < 2:
|
|
414
|
+
raise usage_error("Which profile?")
|
|
415
|
+
name = positionals[1]
|
|
416
|
+
cfg = config_module.load()
|
|
417
|
+
if name not in cfg.get("profiles", {}):
|
|
418
|
+
raise usage_error(
|
|
419
|
+
f'No profile named "{name}".',
|
|
420
|
+
"Run `memorysync config profiles` to see what exists, or "
|
|
421
|
+
f"`memorysync init --profile {name}` to create it.",
|
|
422
|
+
)
|
|
423
|
+
cfg["current_profile"] = name
|
|
424
|
+
config_module.save(cfg)
|
|
425
|
+
return {
|
|
426
|
+
"data": {"current_profile": name},
|
|
427
|
+
"text": lambda: f"{style.green('Switched to')} {name}",
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if sub == "delete-profile":
|
|
431
|
+
if len(positionals) < 2:
|
|
432
|
+
raise usage_error("Which profile?")
|
|
433
|
+
name = positionals[1]
|
|
434
|
+
cfg = config_module.load()
|
|
435
|
+
if name not in cfg.get("profiles", {}):
|
|
436
|
+
raise usage_error(f'No profile named "{name}".')
|
|
437
|
+
del cfg["profiles"][name]
|
|
438
|
+
if cfg.get("current_profile") == name:
|
|
439
|
+
remaining = list(cfg.get("profiles", {}))
|
|
440
|
+
cfg["current_profile"] = remaining[0] if remaining else "default"
|
|
441
|
+
config_module.save(cfg)
|
|
442
|
+
# The stored credential goes with the profile. Leaving it behind would keep
|
|
443
|
+
# a usable key in the keychain for a profile the user just removed.
|
|
444
|
+
credentials.delete_key(name)
|
|
445
|
+
return {
|
|
446
|
+
"data": {"deleted": name},
|
|
447
|
+
"text": lambda: f"{style.green('Deleted profile')} {name}",
|
|
448
|
+
}
|
|
449
|
+
|
|
326
450
|
if sub == "get":
|
|
327
451
|
if len(positionals) < 2:
|
|
328
452
|
raise usage_error("Which key?", "memorysync config get user")
|
|
329
453
|
key = positionals[1]
|
|
330
|
-
|
|
331
|
-
|
|
454
|
+
# Reads the stored profile, not the resolved value. Matches the Node CLI:
|
|
455
|
+
# `config get` answers "what is written down", so an environment variable
|
|
456
|
+
# does not make it look as though something was saved.
|
|
457
|
+
cfg = config_module.load()
|
|
458
|
+
entry = cfg.get("profiles", {}).get(cfg.get("current_profile", "default"), {})
|
|
459
|
+
value = entry.get(key)
|
|
332
460
|
return {"data": {key: value}, "text": lambda: "" if value is None else str(value)}
|
|
333
461
|
|
|
334
|
-
if sub
|
|
335
|
-
if len(positionals) <
|
|
336
|
-
raise usage_error(
|
|
337
|
-
|
|
462
|
+
if sub in {"set", "unset"}:
|
|
463
|
+
if len(positionals) < 2:
|
|
464
|
+
raise usage_error(
|
|
465
|
+
"Which key?", f"Settable: {', '.join(sorted(_SETTABLE))}"
|
|
466
|
+
)
|
|
467
|
+
key = positionals[1]
|
|
338
468
|
if key in {"api_key", "apikey", "key"}:
|
|
339
469
|
raise usage_error(
|
|
340
470
|
"Keys are not stored in the config file.",
|
|
341
471
|
"Run `memorysync init --api-key <key>`; it goes to the OS keychain.",
|
|
342
472
|
)
|
|
343
|
-
|
|
344
|
-
if key not in allowed:
|
|
473
|
+
if key not in _SETTABLE:
|
|
345
474
|
raise usage_error(
|
|
346
|
-
f'
|
|
347
|
-
f"
|
|
475
|
+
f'"{key}" is not settable.',
|
|
476
|
+
f"Settable keys: {', '.join(sorted(_SETTABLE))}. The API key is "
|
|
477
|
+
"never stored here; use `memorysync init`.",
|
|
348
478
|
)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
479
|
+
if sub == "set" and len(positionals) < 3:
|
|
480
|
+
raise usage_error(f"config set {key} <value>")
|
|
481
|
+
value = positionals[2] if sub == "set" else None
|
|
482
|
+
|
|
483
|
+
cfg = config_module.load()
|
|
484
|
+
name = cfg.get("current_profile", "default")
|
|
485
|
+
entry = dict(cfg.get("profiles", {}).get(name, {}))
|
|
486
|
+
if sub == "set":
|
|
487
|
+
entry[key] = value
|
|
488
|
+
else:
|
|
489
|
+
entry.pop(key, None)
|
|
490
|
+
cfg.setdefault("profiles", {})[name] = entry
|
|
491
|
+
config_module.save(cfg)
|
|
354
492
|
return {
|
|
355
|
-
"data": {"profile":
|
|
356
|
-
"text": lambda:
|
|
493
|
+
"data": {"profile": name, key: value},
|
|
494
|
+
"text": lambda: (
|
|
495
|
+
f"{style.green('Set' if sub == 'set' else 'Unset')} {key} "
|
|
496
|
+
f"on profile {name}"
|
|
497
|
+
),
|
|
357
498
|
}
|
|
358
499
|
|
|
359
500
|
if sub == "path":
|
|
360
501
|
path = str(config_module.config_path())
|
|
361
502
|
return {"data": {"path": path}, "text": lambda: path}
|
|
362
503
|
|
|
363
|
-
raise usage_error(
|
|
504
|
+
raise usage_error(
|
|
505
|
+
f'Unknown subcommand "{sub}".', "Try `memorysync config show`."
|
|
506
|
+
)
|
|
@@ -149,9 +149,10 @@ def _init_by_claiming(ctx: dict, profile_name: str) -> dict:
|
|
|
149
149
|
|
|
150
150
|
return {"data": payload, "text": render_complete}
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
# The value ``main`` already resolved from --api-key, the environment, then the
|
|
153
|
+
# stored profile. Resolving it again here would be a second precedence order
|
|
154
|
+
# for the Node CLI to disagree with.
|
|
155
|
+
api_key = ctx.get("api_key")
|
|
155
156
|
if not api_key:
|
|
156
157
|
raise usage_error(
|
|
157
158
|
"No stored key, so there is no evaluation account to claim.",
|
|
@@ -198,6 +199,10 @@ def identify(ctx: dict) -> dict:
|
|
|
198
199
|
entry = data.setdefault("profiles", {}).setdefault(profile_name, {})
|
|
199
200
|
previous = entry.get("user")
|
|
200
201
|
entry["user"] = name
|
|
202
|
+
# Also make it current, matching the Node CLI. Without this, `identify` under
|
|
203
|
+
# one CLI set a default that the other did not switch to, which is exactly the
|
|
204
|
+
# kind of near-miss the two-CLI contract exists to prevent.
|
|
205
|
+
data["current_profile"] = profile_name
|
|
201
206
|
config_module.save(data)
|
|
202
207
|
|
|
203
208
|
payload = {
|
|
@@ -55,7 +55,7 @@ def config_path() -> Path:
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def _empty() -> dict[str, Any]:
|
|
58
|
-
return {"version": CONFIG_VERSION, "profiles": {}}
|
|
58
|
+
return {"version": CONFIG_VERSION, "profiles": {}, "current_profile": "default"}
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
def load() -> dict[str, Any]:
|
|
@@ -78,6 +78,18 @@ def load() -> dict[str, Any]:
|
|
|
78
78
|
return {
|
|
79
79
|
"version": parsed.get("version", CONFIG_VERSION),
|
|
80
80
|
"profiles": parsed.get("profiles") if isinstance(parsed.get("profiles"), dict) else {},
|
|
81
|
+
# Preserved, not dropped. This key was being discarded on read, so the
|
|
82
|
+
# Python CLI had no concept of an active profile at all: `config
|
|
83
|
+
# use-profile work` wrote it and every later command still used "default",
|
|
84
|
+
# and a profile switched under the Node CLI was ignored here. The two are
|
|
85
|
+
# documented as interchangeable and share ~/.memorysync, so that was a real
|
|
86
|
+
# divergence rather than a missing nicety.
|
|
87
|
+
"current_profile": (
|
|
88
|
+
parsed.get("current_profile")
|
|
89
|
+
if isinstance(parsed.get("current_profile"), str)
|
|
90
|
+
and parsed.get("current_profile")
|
|
91
|
+
else "default"
|
|
92
|
+
),
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
|
|
@@ -105,10 +117,18 @@ def resolve(flags: dict[str, Any]) -> dict[str, Any]:
|
|
|
105
117
|
Values are read once here so every command sees the same view, rather than
|
|
106
118
|
each one reaching for the environment at a different moment.
|
|
107
119
|
"""
|
|
120
|
+
# Same precedence as the Node CLI's ``resolveSettings``: an explicit flag, then
|
|
121
|
+
# the environment, then the profile the user selected with
|
|
122
|
+
# ``config use-profile``, then "default". The stored selection was missing here,
|
|
123
|
+
# which is why a switch made under either CLI had no effect on this one.
|
|
124
|
+
config = load()
|
|
108
125
|
profile_name = (
|
|
109
|
-
flags.get("profile")
|
|
126
|
+
flags.get("profile")
|
|
127
|
+
or os.environ.get("MEMORYSYNC_PROFILE")
|
|
128
|
+
or config.get("current_profile")
|
|
129
|
+
or "default"
|
|
110
130
|
)
|
|
111
|
-
stored =
|
|
131
|
+
stored = config.get("profiles", {}).get(profile_name, {})
|
|
112
132
|
|
|
113
133
|
def pick(
|
|
114
134
|
flag_key: str,
|
|
@@ -64,12 +64,22 @@ class ApiClient:
|
|
|
64
64
|
|
|
65
65
|
def headers(self, extra: dict[str, str] | None = None) -> dict[str, str]:
|
|
66
66
|
headers = {
|
|
67
|
-
"X-API-Key": self.api_key,
|
|
68
67
|
"Accept": "application/json",
|
|
69
68
|
# From the package version, never a literal. The Node CLI shipped
|
|
70
69
|
# 1.0.1 announcing itself as 1.0.0 because this was hardcoded.
|
|
71
70
|
"User-Agent": f"memorysync-cli-py/{__version__}",
|
|
72
71
|
}
|
|
72
|
+
# Omitted rather than sent empty when there is no key.
|
|
73
|
+
#
|
|
74
|
+
# Two endpoints are deliberately keyless: minting an evaluation key, and
|
|
75
|
+
# redeeming a claim code. Setting the header to ``None`` made urllib raise
|
|
76
|
+
# ``TypeError: expected string or bytes-like object`` inside ``putheader``,
|
|
77
|
+
# before the request was sent -- so `init --agent` and `init --email --code`
|
|
78
|
+
# could never work in this package at all. The Node CLI survived the same
|
|
79
|
+
# bug only because ``fetch`` coerces null to the string "null", which the
|
|
80
|
+
# server then ignores on an unauthenticated route.
|
|
81
|
+
if self.api_key:
|
|
82
|
+
headers["X-API-Key"] = self.api_key
|
|
73
83
|
# The API rejects key-authenticated memory calls without this, because
|
|
74
84
|
# memory is always scoped to (tenant, project, user) and storing
|
|
75
85
|
# everything under the key owner is not allowed. Commands validate it up
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.1.0"
|
|
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
|