makea-cli 0.2.14__py3-none-any.whl → 0.2.16__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.
makea_cli/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.2.14"
1
+ __version__ = "0.2.16"
makea_cli/api_client.py CHANGED
@@ -5,7 +5,7 @@ import mimetypes
5
5
  from pathlib import Path
6
6
  from pathlib import PurePosixPath
7
7
  from typing import Any
8
- from urllib.parse import unquote
8
+ from urllib.parse import quote, unquote
9
9
 
10
10
  import httpx
11
11
 
@@ -197,8 +197,14 @@ def patch_json(path: str, body: Any, *, timeout: float = 60.0) -> Any:
197
197
  return _request("PATCH", path, json_body=body, timeout=timeout)
198
198
 
199
199
 
200
- def delete_json(path: str, *, params: dict[str, Any] | None = None, timeout: float = 60.0) -> Any:
201
- return _request("DELETE", path, params=params, timeout=timeout)
200
+ def delete_json(
201
+ path: str,
202
+ *,
203
+ params: dict[str, Any] | None = None,
204
+ body: Any = None,
205
+ timeout: float = 60.0,
206
+ ) -> Any:
207
+ return _request("DELETE", path, params=params, json_body=body, timeout=timeout)
202
208
 
203
209
 
204
210
  def send_admin_raw_email(body: dict[str, Any]) -> Any:
@@ -844,6 +850,18 @@ def get_cumulative_payment_and_platform_revenue(
844
850
  )
845
851
 
846
852
 
853
+ def get_fx_rates(*, to: str, sources: list[str], amount: float | None = None) -> Any:
854
+ """
855
+ GET /admin/fx/rates — the platform's own exchange rates (rounded up to its
856
+ RATE_DECIMALS), and with one source and an amount, that amount converted the
857
+ way the platform converts it.
858
+ """
859
+ params: dict[str, Any] = {"to": to, "from": ",".join(sources)}
860
+ if amount is not None:
861
+ params["amount"] = amount
862
+ return get_json("/admin/fx/rates", params=params)
863
+
864
+
847
865
  def extract_ai_tech_specifications(product_reference_id: str, *, timeout: float = 300.0) -> Any:
848
866
  """
849
867
  GET /admin/products/{id}/extract-tech-specifications — synchronous AI extraction;
@@ -1035,6 +1053,51 @@ def admin_delete_library_component(
1035
1053
  )
1036
1054
 
1037
1055
 
1056
+ # --- admin: one person's AI playbook rules ---------------------------------------
1057
+ # Addressed by user_id or email; the backend resolves every scope the person's
1058
+ # rules live in (team, own account, factory) and refuses an email that is on
1059
+ # more than one account. Every write carries a reason the rule keeps.
1060
+
1061
+
1062
+ def _user_rules_path(user: str, rule_id: str | None = None) -> str:
1063
+ path = f"/admin/playbook/users/{quote(user.strip(), safe='@')}/rules"
1064
+ return f"{path}/{quote(rule_id, safe='')}" if rule_id else path
1065
+
1066
+
1067
+ def admin_list_user_rules(
1068
+ user: str, *, status: str | None = None, include_archived: bool = False
1069
+ ) -> Any:
1070
+ """GET /admin/playbook/users/{user}/rules — every rule that holds for one person."""
1071
+ params: dict[str, Any] = {}
1072
+ if status:
1073
+ params["status"] = status
1074
+ if include_archived:
1075
+ params["include_archived"] = "1"
1076
+ return get_json(_user_rules_path(user), params=params or None)
1077
+
1078
+
1079
+ def admin_get_user_rule(user: str, rule_id: str) -> Any:
1080
+ """GET /admin/playbook/users/{user}/rules/{rule_id} — one rule, evidence included."""
1081
+ return get_json(_user_rules_path(user, rule_id))
1082
+
1083
+
1084
+ def admin_create_user_rule(user: str, body: dict[str, Any]) -> Any:
1085
+ """POST /admin/playbook/users/{user}/rules — {text, reason, scope?, applies_to?,
1086
+ when?, mode?, category?, polarity?, status?, force?}."""
1087
+ return post_json(_user_rules_path(user), body)
1088
+
1089
+
1090
+ def admin_update_user_rule(user: str, rule_id: str, body: dict[str, Any]) -> Any:
1091
+ """PATCH /admin/playbook/users/{user}/rules/{rule_id} — {text?, status?,
1092
+ applies_to?, when?, mode?, reason}."""
1093
+ return patch_json(_user_rules_path(user, rule_id), body)
1094
+
1095
+
1096
+ def admin_delete_user_rule(user: str, rule_id: str, reason: str) -> Any:
1097
+ """DELETE /admin/playbook/users/{user}/rules/{rule_id} — archives it, with the reason."""
1098
+ return delete_json(_user_rules_path(user, rule_id), body={"reason": reason})
1099
+
1100
+
1038
1101
  # --- supplier persona: a factory acting on its own RFQs, quotes and orders --------
1039
1102
 
1040
1103
 
@@ -13,6 +13,7 @@ from makea_cli.commands.factory.cmd import register as register_factory
13
13
  from makea_cli.commands.library.cmd import register as register_library
14
14
  from makea_cli.commands.metrics.cmd import register as register_metrics
15
15
  from makea_cli.commands.orders.cmd import register as register_orders
16
+ from makea_cli.commands.playbook.cmd import register as register_playbook
16
17
  from makea_cli.commands.product.cmd import register as register_product
17
18
  from makea_cli.commands.production.cmd import register as register_production
18
19
  from makea_cli.commands.supplier.cmd import register as register_supplier
@@ -33,3 +34,4 @@ def register_all(app: typer.Typer) -> None:
33
34
  register_library(app)
34
35
  register_factory(app)
35
36
  register_brand(app)
37
+ register_playbook(app)
@@ -1,8 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import math
4
-
5
- import httpx
6
3
  import typer
7
4
  from rich.console import Console
8
5
 
@@ -16,38 +13,6 @@ console = Console()
16
13
  _GRANULARITIES = ("day", "week", "month")
17
14
  _ORDER_TYPES = ("all", "sampling", "production")
18
15
 
19
- # Same free FX source the backend uses (FreeCurrencyService), so converted
20
- # amounts match the platform's revenue metrics.
21
- _FX_PRIMARY = (
22
- "https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/{base}.json"
23
- )
24
- _FX_FALLBACK = "https://latest.currency-api.pages.dev/v1/currencies/{base}.json"
25
-
26
-
27
- def _fx_rate(base: str, target: str) -> float:
28
- """Exchange rate base -> target, rounded UP to 2dp exactly like the backend
29
- (math.ceil(raw * 100) / 100) so totals reconcile with revenue metrics."""
30
- base, target = base.lower(), target.lower()
31
- if base == target:
32
- return 1.0
33
- last_err: Exception | None = None
34
- for url in (_FX_PRIMARY.format(base=base), _FX_FALLBACK.format(base=base)):
35
- try:
36
- with httpx.Client(timeout=10) as client:
37
- resp = client.get(url)
38
- resp.raise_for_status()
39
- data = resp.json()
40
- raw = data.get(base, {}).get(target)
41
- if raw is not None:
42
- return math.ceil(float(raw) * 100) / 100
43
- except Exception as e: # noqa: BLE001 - try fallback URL
44
- last_err = e
45
- continue
46
- raise RuntimeError(
47
- f"FX rate {base.upper()}->{target.upper()} unavailable"
48
- + (f": {last_err}" if last_err else "")
49
- )
50
-
51
16
 
52
17
  def register(app: typer.Typer) -> None:
53
18
  @command(
@@ -141,13 +106,13 @@ def register(app: typer.Typer) -> None:
141
106
  "get",
142
107
  "currency-conversion",
143
108
  legacy="convert-currency",
144
- api="GET currency-api",
145
- external="currency-api",
109
+ api="GET /admin/fx/rates",
146
110
  help=(
147
- "Convert money between currencies using the same free FX source and "
148
- "rounding as the platform's revenue metrics. Pass several source "
149
- "currencies via --from (comma-separated) to get a full rate table in "
150
- "one call, then convert locally — avoids repeated lookups."
111
+ "GET /admin/fx/rates — convert money between currencies at the "
112
+ "platform's own rates and rounding, so totals reconcile with its "
113
+ "prices and revenue metrics. Pass several source currencies via --from "
114
+ "(comma-separated) to get a full rate table in one call, then convert "
115
+ "locally — avoids repeated lookups."
151
116
  ),
152
117
  )
153
118
  def convert_currency(
@@ -165,17 +130,20 @@ def register(app: typer.Typer) -> None:
165
130
  help="Optional amount to convert (only meaningful with a single --from).",
166
131
  ),
167
132
  ) -> None:
168
- """Return source->target rates (rounded like the backend), and optionally a
169
- converted amount.
133
+ """Return source->target rates, and optionally a converted amount, as the
134
+ platform computes them.
170
135
 
171
136
  Use it to collapse a mixed-currency set into one currency: collect the
172
137
  distinct currencies, call once with --from a,b,c to get every rate, then
173
- multiply locally. Rates use math.ceil(raw * 100) / 100, matching the
174
- platform's FreeCurrencyService, so totals reconcile with revenue metrics.
175
-
176
- Output JSON: {"to", "rates": {CUR: rate, ...}, "rounding", "source"} plus
177
- {"from", "rate", "amount", "converted"} when a single --from and --amount
178
- are given.
138
+ multiply locally. The rates are the platform's (FreeCurrencyService,
139
+ rounded up to ``rate_decimals``) and come from the backend rather than
140
+ being fetched and rounded here, so they cannot drift from it.
141
+
142
+ Output JSON: {"to", "rates": {CUR: rate, ...}, "rate_decimals",
143
+ "rounding", "source", "unavailable"} plus {"from", "rate", "amount",
144
+ "converted"} when a single --from and --amount are given; "converted" is
145
+ the platform's own conversion, rounding included. A currency with no
146
+ rate fails the command rather than coming back null.
179
147
  """
180
148
  sources = [s.strip() for s in from_.split(",") if s.strip()]
181
149
  if not sources:
@@ -183,22 +151,16 @@ def register(app: typer.Typer) -> None:
183
151
  raise typer.Exit(1)
184
152
 
185
153
  def _do() -> dict:
186
- rates = {s.upper(): _fx_rate(s, to) for s in sources}
187
- out: dict = {
188
- "to": to.upper(),
189
- "rates": rates,
190
- "rounding": "ceil_2dp",
191
- "source": "fawazahmed0/currency-api",
192
- }
193
- if amount is not None and len(sources) == 1:
194
- rate = rates[sources[0].upper()]
195
- out.update(
196
- {
197
- "from": sources[0].upper(),
198
- "rate": rate,
199
- "amount": amount,
200
- "converted": round(amount * rate, 2),
201
- }
154
+ payload = api_client.get_fx_rates(
155
+ to=to,
156
+ sources=sources,
157
+ amount=amount if len(sources) == 1 else None,
158
+ )
159
+ out = (payload or {}).get("result") or {}
160
+ if out.get("unavailable"):
161
+ raise RuntimeError(
162
+ "FX rate unavailable: "
163
+ + ", ".join(f"{s}->{to.upper()}" for s in out["unavailable"])
202
164
  )
203
165
  return out
204
166
 
File without changes
@@ -0,0 +1,329 @@
1
+ """Admin commands for one person's AI playbook rules (Knowledge > Rules).
2
+
3
+ A *rule* is one line Beeboo follows for an account ("Always book DHL Express",
4
+ "Approve PP samples from photos"). Rules live on the brand's team playbook, on
5
+ the person's own account, and, for a factory, on its company playbook. These
6
+ commands address the person (``--user`` takes a user_id or an email) and the
7
+ backend finds every scope; an email on more than one account is refused with
8
+ the candidates, so pass the user_id then.
9
+
10
+ Every write needs ``--reason``. The rule keeps it as an evidence entry
11
+ (``admin_add`` / ``admin_edit`` / ``admin_delete`` with who, why, when and,
12
+ for edits and deletes, what the rule was before), and the audit log records it.
13
+ ``delete`` archives: the rule leaves the brand's Rules page and every prompt,
14
+ the row stays as the record, and ``update --status active`` brings it back.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from typing import Any
21
+
22
+ import typer
23
+ from rich.console import Console
24
+
25
+ from makea_cli import api_client
26
+ from makea_cli.commands._util import print_json_or_exit
27
+ from makea_cli.registry import command
28
+
29
+ console = Console()
30
+
31
+ _STATUSES = ("pending", "active", "muted", "archived")
32
+ _SCOPES = ("org", "user", "supplier")
33
+ _APPLIES_TO_KINDS = ("style", "collection", "supplier", "element")
34
+ _MODES = ("remind", "act")
35
+
36
+ _RULES_EPILOG = """
37
+ --user takes a user_id (Cognito sub) or an email.
38
+
39
+ Scopes: ORG#… is the brand's team playbook, USER#… the person's own,
40
+ SUPPLIER#… a factory's. A new rule goes to the factory playbook for a supplier,
41
+ else the team's (the person's own when there is no team); --scope overrides.
42
+
43
+ --applies-to narrows a rule to one thing: KIND:ID[:LABEL] with KIND one of
44
+ style, collection, supplier, element (e.g. supplier:sup_123:Ningbo Knit).
45
+ "all" (update only) widens it back to the whole account.
46
+
47
+ --when is which products it is for, as JSON:
48
+ '{"categories": ["knitwear"], "service_modes": ["cmt"]}'
49
+ Order conditions the rule already has (units, destination) are kept; {} means
50
+ every product.
51
+
52
+ Examples
53
+ makea-cli admin list rule --user brand@example.com
54
+ makea-cli admin get rule rul_0123456789ab --user brand@example.com
55
+ makea-cli admin create rule --user brand@example.com \\
56
+ --text "Always book DHL Express for samples" --reason "Agreed on the 14 Oct call"
57
+ makea-cli admin update rule rul_0123456789ab --user brand@example.com \\
58
+ --status muted --reason "Brand asked to pause it by email"
59
+ makea-cli admin delete rule rul_0123456789ab --user brand@example.com \\
60
+ --reason "Learned from another brand's thread"
61
+ """.strip()
62
+
63
+
64
+ def _user_option() -> Any:
65
+ return typer.Option(..., "--user", "-u", help="Target person: user_id or email.")
66
+
67
+
68
+ def _reason_option() -> Any:
69
+ return typer.Option(
70
+ ...,
71
+ "--reason",
72
+ "-r",
73
+ help="Why (required). Kept on the rule and in the audit log.",
74
+ )
75
+
76
+
77
+ def _reason(raw: str) -> str:
78
+ reason = " ".join(raw.split())
79
+ if not reason:
80
+ raise typer.BadParameter("--reason must not be empty")
81
+ return reason
82
+
83
+
84
+ def _applies_to(raw: str) -> dict[str, str]:
85
+ if raw.strip().lower() == "all":
86
+ return {"kind": "all"}
87
+ kind, sep, rest = raw.partition(":")
88
+ item_id, _, label = rest.partition(":")
89
+ kind = kind.strip().lower()
90
+ if not sep or kind not in _APPLIES_TO_KINDS or not item_id.strip():
91
+ raise typer.BadParameter(
92
+ f"--applies-to: expected KIND:ID[:LABEL] with KIND one of {', '.join(_APPLIES_TO_KINDS)}"
93
+ )
94
+ out = {"kind": kind, "id": item_id.strip()}
95
+ if label.strip():
96
+ out["label"] = label.strip()
97
+ return out
98
+
99
+
100
+ def _when(raw: str) -> dict[str, Any]:
101
+ try:
102
+ data = json.loads(raw)
103
+ except json.JSONDecodeError as e:
104
+ raise typer.BadParameter(f"--when: invalid JSON ({e})") from e
105
+ if not isinstance(data, dict):
106
+ raise typer.BadParameter("--when: expected a JSON object")
107
+ return data
108
+
109
+
110
+ def _choice(value: str | None, allowed: tuple[str, ...], flag: str) -> str | None:
111
+ if value is None:
112
+ return None
113
+ value = value.strip().lower()
114
+ if value not in allowed:
115
+ raise typer.BadParameter(f"{flag}: one of {', '.join(allowed)}")
116
+ return value
117
+
118
+
119
+ def register(app: typer.Typer) -> None:
120
+ @command(
121
+ app,
122
+ "admin",
123
+ "list",
124
+ "rule",
125
+ api="GET /admin/playbook/users/{user}/rules",
126
+ help=(
127
+ "GET /admin/playbook/users/{user}/rules — every AI playbook rule that holds "
128
+ "for one person (team, own account, factory): status, scope, text, "
129
+ "applies_to/when, mode/effect, source line, dates. Archived ones only with --all."
130
+ ),
131
+ epilog=_RULES_EPILOG,
132
+ )
133
+ def list_rules(
134
+ user: str = _user_option(),
135
+ status: str | None = typer.Option(
136
+ None,
137
+ "--status",
138
+ "-s",
139
+ help=f"Only these statuses, comma-separated ({', '.join(_STATUSES)}).",
140
+ ),
141
+ include_archived: bool = typer.Option(
142
+ False, "--all", help="Also list archived (deleted) rules."
143
+ ),
144
+ ) -> None:
145
+ """List a person's playbook rules."""
146
+ if status:
147
+ for s in status.split(","):
148
+ _choice(s, _STATUSES, "--status")
149
+ print_json_or_exit(
150
+ console,
151
+ lambda: api_client.admin_list_user_rules(
152
+ user, status=status, include_archived=include_archived
153
+ ),
154
+ )
155
+
156
+ @command(
157
+ app,
158
+ "admin",
159
+ "get",
160
+ "rule",
161
+ api="GET /admin/playbook/users/{user}/rules/{rule_id}",
162
+ help=(
163
+ "GET /admin/playbook/users/{user}/rules/{rule_id} — one rule in full, "
164
+ "evidence (where it came from, who changed it and why) included."
165
+ ),
166
+ epilog=_RULES_EPILOG,
167
+ )
168
+ def get_rule(
169
+ rule_id: str = typer.Argument(..., help="Rule id (rul_…)."),
170
+ user: str = _user_option(),
171
+ ) -> None:
172
+ """Show one playbook rule with its evidence."""
173
+ print_json_or_exit(console, lambda: api_client.admin_get_user_rule(user, rule_id))
174
+
175
+ @command(
176
+ app,
177
+ "admin",
178
+ "create",
179
+ "rule",
180
+ api="POST /admin/playbook/users/{user}/rules",
181
+ side_effects=[
182
+ "Beeboo follows the rule in this account's AI answers from now on",
183
+ "shows on the brand's Knowledge > Rules page (brand scopes)",
184
+ ],
185
+ approval="ask",
186
+ reversible=True,
187
+ idempotent=False,
188
+ help=(
189
+ "POST /admin/playbook/users/{user}/rules — add a rule to a person's playbook "
190
+ "(admin-authored, active unless --status). Refuses a repeat or a conflict "
191
+ "unless --force. --reason required."
192
+ ),
193
+ epilog=_RULES_EPILOG,
194
+ )
195
+ def create_rule(
196
+ user: str = _user_option(),
197
+ text: str = typer.Option(..., "--text", help="The rule, one line (max 160 chars)."),
198
+ reason: str = _reason_option(),
199
+ scope: str | None = typer.Option(
200
+ None, "--scope", help=f"Where it lives: {', '.join(_SCOPES)} (default: see below)."
201
+ ),
202
+ applies_to: str | None = typer.Option(
203
+ None, "--applies-to", help="Narrow to one thing: KIND:ID[:LABEL]."
204
+ ),
205
+ when: str | None = typer.Option(
206
+ None,
207
+ "--when",
208
+ help='Which products, JSON: {"categories": [...], "service_modes": [...]}.',
209
+ ),
210
+ mode: str | None = typer.Option(
211
+ None, "--mode", help="remind (default): Beeboo tells; act: Beeboo does it."
212
+ ),
213
+ category: str | None = typer.Option(
214
+ None,
215
+ "--category",
216
+ help="communication, spec_defaults, sourcing, commercial, logistics, process, "
217
+ "hard_constraint, … (default: classified from the text).",
218
+ ),
219
+ polarity: str | None = typer.Option(
220
+ None, "--polarity", help="always, never or prefer (default: from the text)."
221
+ ),
222
+ status: str | None = typer.Option(
223
+ None, "--status", help="Start as pending, active (default) or muted."
224
+ ),
225
+ force: bool = typer.Option(
226
+ False, "--force", help="Add even if it repeats or disagrees with an existing rule."
227
+ ),
228
+ ) -> None:
229
+ """Add a playbook rule for a person."""
230
+ body: dict[str, Any] = {"text": text, "reason": _reason(reason)}
231
+ if scope is not None:
232
+ body["scope"] = _choice(scope, _SCOPES, "--scope")
233
+ if applies_to is not None:
234
+ body["applies_to"] = _applies_to(applies_to)
235
+ if body["applies_to"].get("kind") == "all":
236
+ body.pop("applies_to")
237
+ if when is not None:
238
+ body["when"] = _when(when)
239
+ if mode is not None:
240
+ body["mode"] = _choice(mode, _MODES, "--mode")
241
+ if category is not None:
242
+ body["category"] = category.strip().lower()
243
+ if polarity is not None:
244
+ body["polarity"] = _choice(polarity, ("always", "never", "prefer"), "--polarity")
245
+ if status is not None:
246
+ body["status"] = _choice(status, ("pending", "active", "muted"), "--status")
247
+ if force:
248
+ body["force"] = True
249
+ print_json_or_exit(console, lambda: api_client.admin_create_user_rule(user, body))
250
+
251
+ @command(
252
+ app,
253
+ "admin",
254
+ "update",
255
+ "rule",
256
+ api="PATCH /admin/playbook/users/{user}/rules/{rule_id}",
257
+ side_effects=[
258
+ "changes what Beeboo does for this account",
259
+ "a learned rule becomes authored and stops expiring",
260
+ ],
261
+ approval="ask",
262
+ reversible=True,
263
+ idempotent=True,
264
+ help=(
265
+ "PATCH /admin/playbook/users/{user}/rules/{rule_id} — change a rule's text, "
266
+ "status, applies_to, when or mode. --reason required; the rule keeps what it was before."
267
+ ),
268
+ epilog=_RULES_EPILOG,
269
+ )
270
+ def update_rule(
271
+ rule_id: str = typer.Argument(..., help="Rule id (rul_…)."),
272
+ user: str = _user_option(),
273
+ reason: str = _reason_option(),
274
+ text: str | None = typer.Option(None, "--text", help="New words."),
275
+ status: str | None = typer.Option(None, "--status", help=f"One of {', '.join(_STATUSES)}."),
276
+ applies_to: str | None = typer.Option(
277
+ None, "--applies-to", help='KIND:ID[:LABEL], or "all" for the whole account.'
278
+ ),
279
+ when: str | None = typer.Option(
280
+ None, "--when", help="Which products, JSON ({} for every product)."
281
+ ),
282
+ mode: str | None = typer.Option(None, "--mode", help="remind or act."),
283
+ ) -> None:
284
+ """Edit a person's playbook rule."""
285
+ body: dict[str, Any] = {}
286
+ if text is not None:
287
+ body["text"] = text
288
+ if status is not None:
289
+ body["status"] = _choice(status, _STATUSES, "--status")
290
+ if applies_to is not None:
291
+ body["applies_to"] = _applies_to(applies_to)
292
+ if when is not None:
293
+ body["when"] = _when(when)
294
+ if mode is not None:
295
+ body["mode"] = _choice(mode, _MODES, "--mode")
296
+ if not body:
297
+ raise typer.BadParameter(
298
+ "nothing to change: pass --text, --status, --applies-to, --when or --mode"
299
+ )
300
+ body["reason"] = _reason(reason)
301
+ print_json_or_exit(console, lambda: api_client.admin_update_user_rule(user, rule_id, body))
302
+
303
+ @command(
304
+ app,
305
+ "admin",
306
+ "delete",
307
+ "rule",
308
+ api="DELETE /admin/playbook/users/{user}/rules/{rule_id}",
309
+ side_effects=[
310
+ "archives the rule: off the brand's Rules page and out of every AI prompt",
311
+ "the learner will not propose the same words again",
312
+ ],
313
+ approval="ask",
314
+ reversible=True,
315
+ idempotent=False,
316
+ help=(
317
+ "DELETE /admin/playbook/users/{user}/rules/{rule_id} — remove a rule (archived, "
318
+ "with who and why kept on it; update --status active restores it). --reason required."
319
+ ),
320
+ epilog=_RULES_EPILOG,
321
+ )
322
+ def delete_rule(
323
+ rule_id: str = typer.Argument(..., help="Rule id (rul_…)."),
324
+ user: str = _user_option(),
325
+ reason: str = _reason_option(),
326
+ ) -> None:
327
+ """Remove (archive) a person's playbook rule."""
328
+ why = _reason(reason)
329
+ print_json_or_exit(console, lambda: api_client.admin_delete_user_rule(user, rule_id, why))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: makea-cli
3
- Version: 0.2.14
3
+ Version: 0.2.16
4
4
  Summary: Makea admin CLI (browser login + Makea admin HTTP API)
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: click<9,>=8.1
@@ -92,20 +92,23 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
92
92
  | `makea-cli admin backfill supplier-id` | write(never) | `POST /admin/supplier/backfill_supplier_id_with_quote_requests` | Migrate supplier_id from a wrong UUID to the Cognito user sub (user_id): profile + Cognito supplierReferenceId, optio… | `backfill-supplier-id` |
93
93
  | `makea-cli admin create library-element` | write(auto) | `POST /admin/designer/{user_id}/library_components` | save a reusable component under a designer. Provide --section and --values (see admin get library-schema). | `library-create` |
94
94
  | `makea-cli admin create quote-request` | write(ask) | `POST /admin/supplier/create_quote_request` | create/send a quote request to a supplier for a product (admin). | `create-quote-request` |
95
+ | `makea-cli admin create rule` | write(ask) | `POST /admin/playbook/users/{user}/rules` | add a rule to one person's AI playbook (user_id or email); admin-authored, active unless --status. Refuses a repeat or a conflict unless --force. --reason required. | — |
95
96
  | `makea-cli admin create supplier` | write(ask) | `POST /admin/supplier/add_supplier` | create supplier from JSON file (admin). Run `makea-cli get-supplier-profile-schema` FIRST to get the field taxonomy (… | `add-supplier` |
96
97
  | `makea-cli admin create user` | write(never) | `POST /admin/users/create` | register a user and confirm the Cognito account in one step (no verification email). Allowed --user-type values: "Fas… | `create-user` |
97
98
  | `makea-cli admin delete library-element` | write(never) | `DELETE /admin/designer/{user_id}/library_components/{component_id}` | remove a component from the designer's org library. | `library-delete` |
99
+ | `makea-cli admin delete rule` | write(ask) | `DELETE /admin/playbook/users/{user}/rules/{rule_id}` | remove a rule: archived, with who and why kept on it (`update rule --status active` restores it). --reason required. | — |
98
100
  | `makea-cli admin download document` | read | `GET /admin/document/download` | save a document file (document id + designer user_id). | `download-document` |
99
101
  | `makea-cli admin download product-document` | read | `GET /admin/document/download` | Resolve product via admin list product-order payload, then GET /admin/document/download. | `download-product-document` |
100
102
  | `makea-cli admin generate pricing-groups` | write(ask) | `POST /admin/production_orders/{id}/details/update` | Lay a production order's variants into pricing groups along one axis (--by style \| colour \| variant \| all) with th… | `production-groups-generate` |
101
103
  | `makea-cli admin get active-users` | read | `POST /admin/analytics/query` | Distinct active users per day over the last N days (DAU series). | `analytics-active-users` |
102
104
  | `makea-cli admin get analytics-query` | read | `POST /admin/analytics/query` | Run an arbitrary HogQL query (escape hatch). Example: analytics-query "SELECT count() FROM events" | `analytics-query` |
103
- | `makea-cli admin get currency-conversion` | read | `currency-api` | avoids repeated lookups. | `convert-currency` |
105
+ | `makea-cli admin get currency-conversion` | read | `GET /admin/fx/rates` | the platform's own exchange rates and rounding (rate table for several --from; one --from with --amount converts it). | `convert-currency` |
104
106
  | `makea-cli admin get feature-usage` | read | `POST /admin/analytics/query` | How many distinct users used a feature recently, plus event volume. Matches $pageview path and feature_time_spent eve… | `analytics-feature-usage` |
105
107
  | `makea-cli admin get library-schema` | read | `GET /admin/library_components/schema` | the tech-pack library taxonomy: sections and their library-role field keys (run this before create/update). | `library-schema` |
106
108
  | `makea-cli admin get production-pricing` | read | `GET /admin/production_orders/{id}/details` | show a production order's price tiers, MOQ, sizes/colours, pricing groups and cart coverage. | `production-pricing-show` |
107
109
  | `makea-cli admin get quote-request` | read | `GET /admin/quote_requests/{quote_request_id}` | one quote request in full. | — |
108
110
  | `makea-cli admin get revenue-metrics` | read | `GET /admin/business-metrics/cumulative-payment-and-platform-revenue` | platform-wide payment received and platform revenue (service fee + transaction fee) bucketed over a date range. | `get-cumulative-payment-and-platform-revenue` |
111
+ | `makea-cli admin get rule` | read | `GET /admin/playbook/users/{user}/rules/{rule_id}` | one rule in full, with its evidence: where it came from, who changed it and why. | — |
109
112
  | `makea-cli admin get supplier-profile-schema` | read | `GET /admin/supplier/profile/schema` | supplier profile v2 schema (section/field taxonomy: sections, fields, options, required rules). | `get-supplier-profile-schema` |
110
113
  | `makea-cli admin get tech-spec-extraction` | read | `GET /admin/products/{id}/extract-tech-specifications` | AI-generate specs (not saved). | `extract-ai-tech-spec` |
111
114
  | `makea-cli admin get top-clicks` | read | `POST /admin/analytics/query` | which buttons/elements get clicked most. | `analytics-top-clicks` |
@@ -116,6 +119,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
116
119
  | `makea-cli admin list production-order` | read | `GET /admin/production_orders?type=PRODUCTION` | 大货 (production) orders only. For restock use admin list restock-order; for shipping use admin list shipping-order. | `list-all-production-orders` |
117
120
  | `makea-cli admin list quote-request` | read | `GET /admin/quote_requests` | quote requests, optionally for one product (--product) or one supplier (--supplier, GET /admin/quote_requests/supplie… | — |
118
121
  | `makea-cli admin list restock-order` | read | `GET /admin/production_orders?type=RESTOCK` | restock orders only. Restock shares the production_orders table; this filters to type=RESTOCK. | `list-all-restock-orders` |
122
+ | `makea-cli admin list rule` | read | `GET /admin/playbook/users/{user}/rules` | every AI playbook rule that holds for one person (team, own account, factory): status, scope, text, which products, effect, source. Archived ones only with --all. | — |
119
123
  | `makea-cli admin list sampling-order` | read | `GET /admin/sampling_orders/get_all` | paginated list of all sampling orders (admin). | `list-all-sampling-orders` |
120
124
  | `makea-cli admin list shipping-order` | read | `GET /admin/shipping_orders` | paginated list of all shipping orders (admin). | `list-all-shipping-orders` |
121
125
  | `makea-cli admin list supplier` | read | `GET /admin/supplier/get_all_suppliers` | list all suppliers (admin). | `list-suppliers` |
@@ -129,6 +133,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
129
133
  | `makea-cli admin update library-element` | write(auto) | `PATCH /admin/designer/{user_id}/library_components/{component_id}` | update a component's name and/or values (empty value clears a field). | `library-update` |
130
134
  | `makea-cli admin update quote` | write(ask) | `POST /admin/product/edit_supplier_link_override_info` | admin updates an existing quote (supplier link) in place. Only the options you pass are sent, and the backend leaves … | `update-supplier-quote` |
131
135
  | `makea-cli admin update quote-request` | write(ask) | `PATCH /admin/quote_requests/{quote_request_id}/update` | set a quote request's stage, status and/or due date. Only what you pass is changed. This is the door that unblocks a … | `update-quote-request` |
136
+ | `makea-cli admin update rule` | write(ask) | `PATCH /admin/playbook/users/{user}/rules/{rule_id}` | change a rule's text, status, applies_to, when or mode. --reason required; the rule keeps what it was before. | — |
132
137
  | `makea-cli admin update supplier` | write(ask) | `PUT /admin/supplier/update_supplier` | update an existing supplier from a JSON file (admin). Run `makea-cli get-supplier-profile-schema` FIRST to get the fi… | `update-supplier` |
133
138
  | `makea-cli admin update tech-spec` | write(ask) | `POST /admin/products/{id}/technical-specifications` | persist specs from a JSON file. | `save-product-tech-spec` |
134
139
  | `makea-cli admin update tracking-status` | write(ask) | `POST /admin/tracking/carrier_status` | apply a carrier status looked up by hand (tracking-sweep skill) to one order: status, fan-out, delivered emails (--silent: state only). | — |
@@ -1,6 +1,6 @@
1
- makea_cli/__init__.py,sha256=3fSLgeJpZq4cUgzAH_CdFzXwJEO3NH_VVDv2pQnmwN0,23
1
+ makea_cli/__init__.py,sha256=nS1Qf8CA6pxMzqGCeAMZHTKK2igCSNakZ3o7kDK0JIQ,23
2
2
  makea_cli/__main__.py,sha256=AkQjxWfKE3w9iD1d6I-2Zqvv2t9cct0oYyFF0UiA-Z0,144
3
- makea_cli/api_client.py,sha256=gnxrEoVzv7uJKGe1X0TGKjpVD1XxddoPdPKkLeTQStw,65051
3
+ makea_cli/api_client.py,sha256=I3w5xPMSxZ0bUliXdS8Sz4_W0-ZS3uhHVvoAkn166ZE,67533
4
4
  makea_cli/auth_pkce.py,sha256=iXSyJ7DM3qVuj-3FwPiqM25CxXz9ebFs9FNthZdw3ws,7315
5
5
  makea_cli/broker.py,sha256=5O650nAdRuF-u9E1MuVM2jVv796iBdxJGIaJasxHij0,3019
6
6
  makea_cli/config.py,sha256=hz8MnJSkhxcIEZkcpHR005JrDceo1Xmi8qwIUq50cMU,3003
@@ -10,7 +10,7 @@ makea_cli/main.py,sha256=nuTQ8_P8OSdRbjMHMB4qcKdex124nS-ApCW-zfbqlI0,7169
10
10
  makea_cli/output.py,sha256=VWvZe2Ad8zxT4ZQyBhFRc6wkFwbyYzQFPddepANGAbY,6361
11
11
  makea_cli/posthog_client.py,sha256=k0cy9Hq7bhdLlPvC1ZW_esJ70xHDdQzPQUlx-GpVskk,1921
12
12
  makea_cli/registry.py,sha256=GYJK_idhwIh1X2ictwFjn8PEMaXT56hFVkoLEVYrVqw,13722
13
- makea_cli/commands/__init__.py,sha256=sxDNEYDmQTASzH2uvMtqNwKLLbOQBzwPfxc-S1tHf-w,1474
13
+ makea_cli/commands/__init__.py,sha256=LJ6pjYtIL_VvdSRbF1QomSojcarJF710Sifg2RWsgQo,1575
14
14
  makea_cli/commands/_util.py,sha256=zSFwWaWpncARlKxgq4WuJlJvNv7XdZ39HfHrNOELHuw,980
15
15
  makea_cli/commands/analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  makea_cli/commands/analytics/cmd.py,sha256=KVLUtYSeyYZ8_09jIuIAmePjQqcpQS2mb5PBBzGwQYQ,6562
@@ -31,9 +31,11 @@ makea_cli/commands/factory/cmd.py,sha256=k_UGkqpzxxZhN9WjKt1rb95KOTGHMpMJRpfNHTG
31
31
  makea_cli/commands/library/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  makea_cli/commands/library/cmd.py,sha256=eXZa6iLyPaoEWtRd-UmhRT6Ox-zlfs89gJqGn-IPuSU,11187
33
33
  makea_cli/commands/metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- makea_cli/commands/metrics/cmd.py,sha256=mXMSy3zHgJXzqLNy6OrdQqySk9COknPKVDf_SQQdFIA,7662
34
+ makea_cli/commands/metrics/cmd.py,sha256=ox5OD7Bk3QJcdHQv5u4t6G-OlC6eSbQG5BEH1yBkxno,6414
35
35
  makea_cli/commands/orders/__init__.py,sha256=6WKPqX02e0RRPmItPIBJ0TA4_yDI2PVvBDkAiMQu9rU,75
36
36
  makea_cli/commands/orders/cmd.py,sha256=Nh5lGv5k4SIwsr_hg-ndah8eTorZCYIMpTAz4CCi6oY,13352
37
+ makea_cli/commands/playbook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
+ makea_cli/commands/playbook/cmd.py,sha256=ktSn3Fx1i6GN4eGeqp4nW0OJ85oduIa3J10mDNRPRMo,12548
37
39
  makea_cli/commands/product/__init__.py,sha256=mfdonFmpbddDSlJq6JWNWgNpM6aOpKtZr-3FJ6scrwg,76
38
40
  makea_cli/commands/product/cmd.py,sha256=W0oVTZvsK1QWZWv2x6NC26_JQMXrO16O8gYuKYPWC08,7101
39
41
  makea_cli/commands/production/__init__.py,sha256=Whn5ziwVMt4jzcokiFWLhWIpRLYC-ldnjZgAzeQ3We4,79
@@ -41,7 +43,7 @@ makea_cli/commands/production/cmd.py,sha256=s6fVAGzOvokF6Va5WKGEA2vw2x2hDQ3PrsF0
41
43
  makea_cli/commands/production/pricing_spec.py,sha256=xGbj7-ps3PcKuU6Oc59A70b62FjF6Awhr5IY3ymZveU,27174
42
44
  makea_cli/commands/supplier/__init__.py,sha256=qfQDeQdQ-B7I_uwFVfcp1vXFUTL4fAcNLu3ipsZ7dfs,77
43
45
  makea_cli/commands/supplier/cmd.py,sha256=crRCy9WfhkryHMIL3iufoiT90_SG3-mKChAtdhd06uc,58076
44
- makea_cli-0.2.14.dist-info/METADATA,sha256=E4VzOb4JNgtC6tRz49sOdRKb3ZW2IqrEZJ6HFEgi6Wg,31730
45
- makea_cli-0.2.14.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
46
- makea_cli-0.2.14.dist-info/entry_points.txt,sha256=4x-lbVa1jBbNlkNq7L-rYyfRh2spZmc3BLIzP-Dl-Qk,50
47
- makea_cli-0.2.14.dist-info/RECORD,,
46
+ makea_cli-0.2.16.dist-info/METADATA,sha256=-Zpj9hQq0JaECBulxaELeFJeaMEjjthhrIDO8VxBeKI,32987
47
+ makea_cli-0.2.16.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
48
+ makea_cli-0.2.16.dist-info/entry_points.txt,sha256=4x-lbVa1jBbNlkNq7L-rYyfRh2spZmc3BLIzP-Dl-Qk,50
49
+ makea_cli-0.2.16.dist-info/RECORD,,