makea-cli 0.2.1__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {makea_cli-0.2.1 → makea_cli-0.2.4}/CHANGELOG.md +53 -0
  2. {makea_cli-0.2.1 → makea_cli-0.2.4}/PKG-INFO +13 -1
  3. {makea_cli-0.2.1 → makea_cli-0.2.4}/README.md +12 -0
  4. makea_cli-0.2.4/makea_cli/__init__.py +1 -0
  5. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/api_client.py +76 -2
  6. makea_cli-0.2.4/makea_cli/broker.py +83 -0
  7. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/brand/cmd.py +288 -0
  8. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/supplier/cmd.py +24 -4
  9. {makea_cli-0.2.1 → makea_cli-0.2.4}/pyproject.toml +1 -1
  10. {makea_cli-0.2.1 → makea_cli-0.2.4}/tests/golden_manifest.json +458 -1
  11. makea_cli-0.2.4/tests/test_broker.py +106 -0
  12. {makea_cli-0.2.1 → makea_cli-0.2.4}/tests/test_persona_commands.py +108 -0
  13. makea_cli-0.2.1/makea_cli/__init__.py +0 -1
  14. {makea_cli-0.2.1 → makea_cli-0.2.4}/.claude/skills/create-supplier-profile/SKILL.md +0 -0
  15. {makea_cli-0.2.1 → makea_cli-0.2.4}/.claude/skills/lark-cli-setup/SKILL.md +0 -0
  16. {makea_cli-0.2.1 → makea_cli-0.2.4}/.claude/skills/link-supplier-user/SKILL.md +0 -0
  17. {makea_cli-0.2.1 → makea_cli-0.2.4}/.claude/skills/makea-cli-setup/SKILL.md +0 -0
  18. {makea_cli-0.2.1 → makea_cli-0.2.4}/.claude/skills/payment-revenue-report/SKILL.md +0 -0
  19. {makea_cli-0.2.1 → makea_cli-0.2.4}/.claude/skills/supplier-user-id-migration/SKILL.md +0 -0
  20. {makea_cli-0.2.1 → makea_cli-0.2.4}/.gitignore +0 -0
  21. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea-collection-upload/SKILL.md +0 -0
  22. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea-collection-upload/scripts/create_products.py +0 -0
  23. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/__main__.py +0 -0
  24. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/auth_pkce.py +0 -0
  25. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/__init__.py +0 -0
  26. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/_util.py +0 -0
  27. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/analytics/__init__.py +0 -0
  28. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/analytics/cmd.py +0 -0
  29. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/auth/__init__.py +0 -0
  30. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/auth/cmd.py +0 -0
  31. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/brand/__init__.py +0 -0
  32. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/designer/__init__.py +0 -0
  33. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/designer/cmd.py +0 -0
  34. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/directory/__init__.py +0 -0
  35. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/directory/cmd.py +0 -0
  36. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/document/__init__.py +0 -0
  37. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/document/cmd.py +0 -0
  38. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/email/__init__.py +0 -0
  39. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/email/cmd.py +0 -0
  40. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/factory/__init__.py +0 -0
  41. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/factory/cmd.py +0 -0
  42. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/library/__init__.py +0 -0
  43. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/library/cmd.py +0 -0
  44. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/metrics/__init__.py +0 -0
  45. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/metrics/cmd.py +0 -0
  46. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/orders/__init__.py +0 -0
  47. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/orders/cmd.py +0 -0
  48. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/product/__init__.py +0 -0
  49. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/product/cmd.py +0 -0
  50. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/production/__init__.py +0 -0
  51. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/production/cmd.py +0 -0
  52. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/production/pricing_spec.py +0 -0
  53. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/commands/supplier/__init__.py +0 -0
  54. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/config.py +0 -0
  55. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/errors.py +0 -0
  56. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/identity.py +0 -0
  57. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/main.py +0 -0
  58. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/output.py +0 -0
  59. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/posthog_client.py +0 -0
  60. {makea_cli-0.2.1 → makea_cli-0.2.4}/makea_cli/registry.py +0 -0
  61. {makea_cli-0.2.1 → makea_cli-0.2.4}/npm/makea-cli/README.md +0 -0
  62. {makea_cli-0.2.1 → makea_cli-0.2.4}/npm/makea-cli/makea-cli.mjs +0 -0
  63. {makea_cli-0.2.1 → makea_cli-0.2.4}/npm/makea-cli/package.json +0 -0
  64. {makea_cli-0.2.1 → makea_cli-0.2.4}/tests/test_output_contract.py +0 -0
  65. {makea_cli-0.2.1 → makea_cli-0.2.4}/tests/test_registry.py +0 -0
  66. {makea_cli-0.2.1 → makea_cli-0.2.4}/uv.lock +0 -0
@@ -1,5 +1,58 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.4
4
+
5
+ For Beeboo acting for a brand through the CLI (the Claude agent on the product
6
+ channel).
7
+
8
+ - **Token broker mode.** With `MAKEA_TOKEN_BROKER=<unix socket>` the CLI sends
9
+ its own argv to the broker and uses the token it gets back
10
+ (`x-impersonation-token`) for that one invocation. There is no fallback to a
11
+ cached login in this mode: a command that did not come through the host's
12
+ gate gets no token.
13
+ - `designer submit tech-pack <product_id>` (`ask`): confirm the tech pack.
14
+ - `designer create quote-award <product_id> --quote <supplier link id>` (`ask`):
15
+ choose the quote to sample with. Creates or reuses a DRAFT sampling order and
16
+ prints its id.
17
+ - `designer create checkout --key sampling:<id> [--key …] --accept-terms`
18
+ (`ask`): a Stripe checkout link for one or more charges (sampling,
19
+ production deposit/final, shipping, misc). It is the one command near a
20
+ payment endpoint: it creates the session and nothing is charged until the
21
+ brand pays on Stripe's page. Bank transfers and subscriptions stay out.
22
+
23
+ ## 0.2.3
24
+
25
+ - `admin update quote --ip-schedule-hidden/--ip-schedule-visible`: hide a
26
+ supplier's IP Schedule from the customer, or show it again. Visible unless set.
27
+ - `--ip-schedule-json` help describes the version 2 schedule: two buckets
28
+ (`production_files`, tech pack and BOM included, and `physical_tooling`),
29
+ "not created" by default, and optional.
30
+
31
+ ## 0.2.2
32
+
33
+ Order lifecycle, exposed in tiers rather than all at once.
34
+
35
+ - `ask` (an agent must put it in front of a person): `designer submit
36
+ sampling-order` (confirm a sample order), `designer submit production-order`,
37
+ `designer update production-order --complete|--links-file`, `designer update
38
+ shipping-order --confirm-receive`.
39
+ - `never` (an agent must not run it at all): `designer update sampling-order
40
+ --cancel`, `designer delete sampling-order` (DRAFT only, the backend enforces
41
+ that), `designer delete shipping-order` — the endpoint is named cancel but it
42
+ unlinks every production order and removes the shipment.
43
+
44
+ Cancel and delete are separate commands rather than flags on an `ask` one. The
45
+ grade is per command, so putting them on a milder command would launder a
46
+ destructive action through the wrong grade.
47
+
48
+ **The grade is advice to a harness, not a gate in the CLI.** A person in a
49
+ terminal runs any of these; `never` means an agent may not decide to.
50
+
51
+ Payments, bank transfers and subscriptions stay out of the CLI. The endpoints
52
+ exist, and a `--dry-run` would even make them look safe, but giving them a
53
+ command puts a charge one step away from an agent and no approval grade makes
54
+ that a good trade. A test asserts no write command reaches them.
55
+
3
56
  ## 0.2.1
4
57
 
5
58
  Each persona can now clear its own queue. `list task` and `list action` could show
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: makea-cli
3
- Version: 0.2.1
3
+ Version: 0.2.4
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
@@ -79,6 +79,8 @@ makea-cli auth --logout
79
79
 
80
80
  每条命令都有的开关:`--describe` 打印它的机器可读定义(flags、副作用、审批档位);`--json` 强制纯 JSON(stdout 不是终端时默认就是)。读命令还有 `--fields a,b,c` 只保留这些字段;`list` 命令还有 `--limit N`(后端自己分页的就透传,不分页的在本地截断并在结果里写明 `truncated` / `returned` / `omitted`);写命令还有 `--dry-run`(打印将要发出的请求,不发)和 `--idempotency-key <k>`。`makea-cli --describe --json` 打印全量 manifest。
81
81
 
82
+ 写命令的**审批档位**(manifest 的 `approval`)是给 harness 的建议,不是 CLI 的门:`auto` 可以自动跑;`ask` 必须先让人确认;`never` agent 不许自己决定(人在终端照常能跑)。动钱的端点(payments / bank transfer / subscription)**故意不进 CLI**。
83
+
82
84
  exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不匹配 · `4` 后端拒绝(4xx,错误 JSON 在 stderr)· `5` 没跑完(5xx / 网络 / 超时)——读命令直接重试即可;**写命令可能已经生效了,重试前先读一遍状态**。
83
85
 
84
86
  0.1 的平铺命令名(`list-users`、`submit-supplier-quote` …)仍可用,作为隐藏的过渡别名转发到新命令并在 stderr 打一行提示;**下一个 minor 版本删除**。完整的改名对照表在 [CHANGELOG.md](CHANGELOG.md)。
@@ -142,11 +144,15 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
142
144
 
143
145
  | 命令 | 读/写 | 后端 | 说明 | 旧名 |
144
146
  |---|---|---|---|---|
147
+ | `makea-cli designer create checkout` | write(ask) | `POST /designer/payments/basket/create-session` | a Stripe checkout link for one or more charges. --key repeats: sampling:<sampling_order_id>, production:<production_o… | — |
145
148
  | `makea-cli designer create collection` | write(auto) | `POST /designer/collections/create` | create or update a designer collection. | `designer-create-collection` |
146
149
  | `makea-cli designer create note` | write(ask) | `POST /designer/products/{product_id}/notes` | post a message into a product's conversation. | — |
147
150
  | `makea-cli designer create product` | write(ask) | `POST /designer/product/create_or_update_product` | payload must match manufacture/specifications (all frontend fields; categories/subcategories + collection_id in --hel… | `designer-upsert-product` |
151
+ | `makea-cli designer create quote-award` | write(ask) | `POST /designer/quotes/group_pay_prepare` | choose the supplier quote to sample with. --quote is the supplier link id from `designer list supplier-link`; repeat … | — |
148
152
  | `makea-cli designer create rfq` | write(ask) | `POST /designer/product/send_rfq` | ask the chosen suppliers to quote on a product. --supplier repeats; --makea-sourcing also asks Makea to source. | — |
149
153
  | `makea-cli designer create sampling-order` | write(ask) | `POST /designer/sampling_orders` | order samples. --file is the full order payload (product_order_id, items, fees …); run --dry-run first and read the b… | — |
154
+ | `makea-cli designer delete sampling-order` | write(never) | `DELETE /designer/sampling_orders/{sampling_order_id}` | delete a DRAFT sample order. The backend refuses any other status. Graded `never`. | — |
155
+ | `makea-cli designer delete shipping-order` | write(never) | `POST /designer/shipping_orders/{shipping_order_id}/cancel` | the endpoint is called cancel but it unlinks every production order and removes the shipment, so it is a delete here.… | — |
150
156
  | `makea-cli designer download document` | read | `GET /designer/document/download` | save bytes for user_id + document_id (designer-authenticated; same query contract as admin document download). | `designer-download-document` |
151
157
  | `makea-cli designer get collection` | read | `GET /designer/collections/{collection_id}/overview` | one collection with its products, orders and lifecycle in a single read. | — |
152
158
  | `makea-cli designer get lifecycle` | read | `GET /designer/product_orders/{product_order_id}/lifecycle` | which stage this product is in. | — |
@@ -167,10 +173,16 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
167
173
  | `makea-cli designer list task` | read | `GET /designer/tasks` | what the platform is waiting on you for. | — |
168
174
  | `makea-cli designer search note` | read | `GET /designer/products/{product_id}/channel_search` | full-text search across a product's conversation; --channel narrows to one channel key. | — |
169
175
  | `makea-cli designer set product-collection` | write(auto) | `POST /designer/products/{id}/link-to-collection` | assign product to a collection. | `designer-link-product-to-collection` |
176
+ | `makea-cli designer submit production-order` | write(ask) | `POST /designer/production_orders/{production_order_id}/submit` | submit a 大货 order. This is the commitment, so --dry-run and confirm with the user first. | — |
177
+ | `makea-cli designer submit sampling-order` | write(ask) | `POST /designer/sampling_orders/{sampling_order_id}/confirm` | confirm a sample order so the supplier starts. Run --dry-run first and read it back to the user. | — |
178
+ | `makea-cli designer submit tech-pack` | write(ask) | `POST /designer/products/{product_id}/technical-specifications/confirm` | confirm the tech pack, the step before anyone is asked to quote. | — |
170
179
  | `makea-cli designer update action` | write(auto) | `POST /designer/actions/{action_id}/{acknowledge\|snooze\|dismiss}` | respond to one of the pending actions from `designer list action`. An action is closed by the fact it asks for, not b… | — |
171
180
  | `makea-cli designer update note` | write(ask) | `POST /designer/products/{product_id}/notes/{note_id}/{accept\|reject}` | decide on a task the assistant proposed on a product. | — |
172
181
  | `makea-cli designer update product` | write(auto) | `POST /designer/product/update_basic_info` | patch fields on a product. --cover-image / --back-image take a document id and are what the assistant's set_product_i… | — |
182
+ | `makea-cli designer update production-order` | write(ask) | `POST /designer/production_orders/{production_order_id}/complete` | POST /designer/production_orders/{id}/complete, or PUT the order to re-pick which supplier links it covers (--links-f… | — |
173
183
  | `makea-cli designer update proposal` | write(ask) | `POST /designer/beeboo_proposals/approve` | apply one spec change the assistant proposed. This edits the pack, so read the proposal first. | — |
184
+ | `makea-cli designer update sampling-order` | write(never) | `POST /designer/sampling_orders/{sampling_order_id}/cancel` | cancel a sample order. Graded `never`: an agent must not do this on its own, a person runs it. | — |
185
+ | `makea-cli designer update shipping-order` | write(ask) | `POST /designer/shipping_orders/{shipping_order_id}/confirm_receive` | say the goods arrived. The backend accepts this only while the shipment is IN_TRANSIT. | — |
174
186
  | `makea-cli designer update suggested-action` | write(ask) | `POST /designer/channels/{channel_id}/suggested_actions/bulk_accept` | accept several of a channel's suggested actions in one write. --action takes note_id:action_id and repeats. | — |
175
187
  | `makea-cli designer update task` | write(auto) | `POST /designer/tasks/{complete\|dismiss}` | close one of the tasks from `designer list task`. Complete means you did it; dismiss means it does not apply. | — |
176
188
  | `makea-cli designer upload document` | write(auto) | `POST /designer/document/upload` | upload a file for the current designer token; optional product_id (omit to start a new draft id server-side). | `designer-upload-document` |
@@ -67,6 +67,8 @@ makea-cli auth --logout
67
67
 
68
68
  每条命令都有的开关:`--describe` 打印它的机器可读定义(flags、副作用、审批档位);`--json` 强制纯 JSON(stdout 不是终端时默认就是)。读命令还有 `--fields a,b,c` 只保留这些字段;`list` 命令还有 `--limit N`(后端自己分页的就透传,不分页的在本地截断并在结果里写明 `truncated` / `returned` / `omitted`);写命令还有 `--dry-run`(打印将要发出的请求,不发)和 `--idempotency-key <k>`。`makea-cli --describe --json` 打印全量 manifest。
69
69
 
70
+ 写命令的**审批档位**(manifest 的 `approval`)是给 harness 的建议,不是 CLI 的门:`auto` 可以自动跑;`ask` 必须先让人确认;`never` agent 不许自己决定(人在终端照常能跑)。动钱的端点(payments / bank transfer / subscription)**故意不进 CLI**。
71
+
70
72
  exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不匹配 · `4` 后端拒绝(4xx,错误 JSON 在 stderr)· `5` 没跑完(5xx / 网络 / 超时)——读命令直接重试即可;**写命令可能已经生效了,重试前先读一遍状态**。
71
73
 
72
74
  0.1 的平铺命令名(`list-users`、`submit-supplier-quote` …)仍可用,作为隐藏的过渡别名转发到新命令并在 stderr 打一行提示;**下一个 minor 版本删除**。完整的改名对照表在 [CHANGELOG.md](CHANGELOG.md)。
@@ -130,11 +132,15 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
130
132
 
131
133
  | 命令 | 读/写 | 后端 | 说明 | 旧名 |
132
134
  |---|---|---|---|---|
135
+ | `makea-cli designer create checkout` | write(ask) | `POST /designer/payments/basket/create-session` | a Stripe checkout link for one or more charges. --key repeats: sampling:<sampling_order_id>, production:<production_o… | — |
133
136
  | `makea-cli designer create collection` | write(auto) | `POST /designer/collections/create` | create or update a designer collection. | `designer-create-collection` |
134
137
  | `makea-cli designer create note` | write(ask) | `POST /designer/products/{product_id}/notes` | post a message into a product's conversation. | — |
135
138
  | `makea-cli designer create product` | write(ask) | `POST /designer/product/create_or_update_product` | payload must match manufacture/specifications (all frontend fields; categories/subcategories + collection_id in --hel… | `designer-upsert-product` |
139
+ | `makea-cli designer create quote-award` | write(ask) | `POST /designer/quotes/group_pay_prepare` | choose the supplier quote to sample with. --quote is the supplier link id from `designer list supplier-link`; repeat … | — |
136
140
  | `makea-cli designer create rfq` | write(ask) | `POST /designer/product/send_rfq` | ask the chosen suppliers to quote on a product. --supplier repeats; --makea-sourcing also asks Makea to source. | — |
137
141
  | `makea-cli designer create sampling-order` | write(ask) | `POST /designer/sampling_orders` | order samples. --file is the full order payload (product_order_id, items, fees …); run --dry-run first and read the b… | — |
142
+ | `makea-cli designer delete sampling-order` | write(never) | `DELETE /designer/sampling_orders/{sampling_order_id}` | delete a DRAFT sample order. The backend refuses any other status. Graded `never`. | — |
143
+ | `makea-cli designer delete shipping-order` | write(never) | `POST /designer/shipping_orders/{shipping_order_id}/cancel` | the endpoint is called cancel but it unlinks every production order and removes the shipment, so it is a delete here.… | — |
138
144
  | `makea-cli designer download document` | read | `GET /designer/document/download` | save bytes for user_id + document_id (designer-authenticated; same query contract as admin document download). | `designer-download-document` |
139
145
  | `makea-cli designer get collection` | read | `GET /designer/collections/{collection_id}/overview` | one collection with its products, orders and lifecycle in a single read. | — |
140
146
  | `makea-cli designer get lifecycle` | read | `GET /designer/product_orders/{product_order_id}/lifecycle` | which stage this product is in. | — |
@@ -155,10 +161,16 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
155
161
  | `makea-cli designer list task` | read | `GET /designer/tasks` | what the platform is waiting on you for. | — |
156
162
  | `makea-cli designer search note` | read | `GET /designer/products/{product_id}/channel_search` | full-text search across a product's conversation; --channel narrows to one channel key. | — |
157
163
  | `makea-cli designer set product-collection` | write(auto) | `POST /designer/products/{id}/link-to-collection` | assign product to a collection. | `designer-link-product-to-collection` |
164
+ | `makea-cli designer submit production-order` | write(ask) | `POST /designer/production_orders/{production_order_id}/submit` | submit a 大货 order. This is the commitment, so --dry-run and confirm with the user first. | — |
165
+ | `makea-cli designer submit sampling-order` | write(ask) | `POST /designer/sampling_orders/{sampling_order_id}/confirm` | confirm a sample order so the supplier starts. Run --dry-run first and read it back to the user. | — |
166
+ | `makea-cli designer submit tech-pack` | write(ask) | `POST /designer/products/{product_id}/technical-specifications/confirm` | confirm the tech pack, the step before anyone is asked to quote. | — |
158
167
  | `makea-cli designer update action` | write(auto) | `POST /designer/actions/{action_id}/{acknowledge\|snooze\|dismiss}` | respond to one of the pending actions from `designer list action`. An action is closed by the fact it asks for, not b… | — |
159
168
  | `makea-cli designer update note` | write(ask) | `POST /designer/products/{product_id}/notes/{note_id}/{accept\|reject}` | decide on a task the assistant proposed on a product. | — |
160
169
  | `makea-cli designer update product` | write(auto) | `POST /designer/product/update_basic_info` | patch fields on a product. --cover-image / --back-image take a document id and are what the assistant's set_product_i… | — |
170
+ | `makea-cli designer update production-order` | write(ask) | `POST /designer/production_orders/{production_order_id}/complete` | POST /designer/production_orders/{id}/complete, or PUT the order to re-pick which supplier links it covers (--links-f… | — |
161
171
  | `makea-cli designer update proposal` | write(ask) | `POST /designer/beeboo_proposals/approve` | apply one spec change the assistant proposed. This edits the pack, so read the proposal first. | — |
172
+ | `makea-cli designer update sampling-order` | write(never) | `POST /designer/sampling_orders/{sampling_order_id}/cancel` | cancel a sample order. Graded `never`: an agent must not do this on its own, a person runs it. | — |
173
+ | `makea-cli designer update shipping-order` | write(ask) | `POST /designer/shipping_orders/{shipping_order_id}/confirm_receive` | say the goods arrived. The backend accepts this only while the shipment is IN_TRANSIT. | — |
162
174
  | `makea-cli designer update suggested-action` | write(ask) | `POST /designer/channels/{channel_id}/suggested_actions/bulk_accept` | accept several of a channel's suggested actions in one write. --action takes note_id:action_id and repeats. | — |
163
175
  | `makea-cli designer update task` | write(auto) | `POST /designer/tasks/{complete\|dismiss}` | close one of the tasks from `designer list task`. Complete means you did it; dismiss means it does not apply. | — |
164
176
  | `makea-cli designer upload document` | write(auto) | `POST /designer/document/upload` | upload a file for the current designer token; optional product_id (omit to start a new draft id server-side). | `designer-upload-document` |
@@ -0,0 +1 @@
1
+ __version__ = "0.2.4"
@@ -12,6 +12,7 @@ import httpx
12
12
  from contextvars import ContextVar
13
13
  from dataclasses import dataclass
14
14
 
15
+ from makea_cli import broker
15
16
  from makea_cli.auth_pkce import ensure_fresh_id_token
16
17
  from makea_cli.config import api_base_url
17
18
  from makea_cli.errors import ApiError, AuthError, UnknownOutcomeError
@@ -53,8 +54,20 @@ def _token() -> str:
53
54
  raise AuthError(str(e)) from e
54
55
 
55
56
 
57
+ def _auth_headers() -> dict[str, str]:
58
+ """Who this request is from.
59
+
60
+ Under a token broker the token is a delegation the backend reads from the
61
+ impersonation header; there is no bearer at all, so a command that did not
62
+ come through the host's gate has nothing to send.
63
+ """
64
+ if broker.broker_path():
65
+ return {"x-impersonation-token": broker.brokered_token() or ""}
66
+ return {"Authorization": f"Bearer {_token()}"}
67
+
68
+
56
69
  def _headers() -> dict[str, str]:
57
- headers = {"Authorization": f"Bearer {_token()}", "Accept": "application/json"}
70
+ headers = {**_auth_headers(), "Accept": "application/json"}
58
71
  if state.idempotency_key:
59
72
  headers["Idempotency-Key"] = state.idempotency_key
60
73
  return headers
@@ -254,7 +267,7 @@ def post_multipart_with_files(
254
267
  def _download(path: str, params: dict[str, Any], *, timeout: float) -> tuple[bytes, str | None]:
255
268
  """GET a file (or follow the redirect to its presigned URL); returns (bytes, filename)."""
256
269
  url = f"{api_base_url()}{path}"
257
- headers = {"Authorization": f"Bearer {_token()}", "Accept": "*/*"}
270
+ headers = {**_auth_headers(), "Accept": "*/*"}
258
271
  try:
259
272
  with httpx.Client(timeout=timeout, follow_redirects=True) as client:
260
273
  r = client.get(url, headers=headers, params=params)
@@ -1185,6 +1198,21 @@ def designer_send_rfq(body: dict[str, Any]) -> Any:
1185
1198
  return post_json("/designer/product/send_rfq", body)
1186
1199
 
1187
1200
 
1201
+ def designer_confirm_tech_spec(product_id: str) -> Any:
1202
+ """POST /designer/products/{id}/technical-specifications/confirm — confirm the tech pack."""
1203
+ return post_json(f"/designer/products/{product_id}/technical-specifications/confirm", {})
1204
+
1205
+
1206
+ def designer_award_quotes(body: dict[str, Any]) -> Any:
1207
+ """POST /designer/quotes/group_pay_prepare — DRAFT sampling orders for chosen quotes."""
1208
+ return post_json("/designer/quotes/group_pay_prepare", body)
1209
+
1210
+
1211
+ def designer_create_checkout(body: dict[str, Any]) -> Any:
1212
+ """POST /designer/payments/basket/create-session — a Stripe checkout for charges."""
1213
+ return post_json("/designer/payments/basket/create-session", body)
1214
+
1215
+
1188
1216
  def designer_list_collections() -> Any:
1189
1217
  """GET /designer/collections — the brand's collections."""
1190
1218
  return get_json("/designer/collections")
@@ -1312,3 +1340,49 @@ def designer_list_actions(*, product_id: str | None = None) -> Any:
1312
1340
  def supplier_list_tasks() -> Any:
1313
1341
  """GET /supplier/tasks — the factory's onboarding and per-order to-dos."""
1314
1342
  return get_json("/supplier/tasks")
1343
+
1344
+
1345
+ # --- order lifecycle (designer). Tiered on purpose: see the commands' approval
1346
+ # --- grades -- submit/confirm ask a person, cancel/delete are never automatic,
1347
+ # --- and nothing here moves money. Payments and subscriptions are deliberately
1348
+ # --- absent from the CLI: an agent should not have a path to a charge.
1349
+
1350
+
1351
+ def designer_confirm_sampling_order(sampling_order_id: str) -> Any:
1352
+ """POST /designer/sampling_orders/{id}/confirm — the brand confirms the sample order."""
1353
+ return post_json(f"/designer/sampling_orders/{sampling_order_id}/confirm", {})
1354
+
1355
+
1356
+ def designer_cancel_sampling_order(sampling_order_id: str) -> Any:
1357
+ """POST /designer/sampling_orders/{id}/cancel — cancel it."""
1358
+ return post_json(f"/designer/sampling_orders/{sampling_order_id}/cancel", {})
1359
+
1360
+
1361
+ def designer_delete_sampling_order(sampling_order_id: str) -> Any:
1362
+ """DELETE /designer/sampling_orders/{id} — DRAFT only; the backend refuses anything else."""
1363
+ return delete_json(f"/designer/sampling_orders/{sampling_order_id}")
1364
+
1365
+
1366
+ def designer_submit_production_order(production_order_id: str) -> Any:
1367
+ """POST /designer/production_orders/{id}/submit — hand the bulk order to the supplier."""
1368
+ return post_json(f"/designer/production_orders/{production_order_id}/submit", {})
1369
+
1370
+
1371
+ def designer_complete_production_order(production_order_id: str) -> Any:
1372
+ """POST /designer/production_orders/{id}/complete — the brand marks the bulk order done."""
1373
+ return post_json(f"/designer/production_orders/{production_order_id}/complete", {})
1374
+
1375
+
1376
+ def designer_update_production_order(production_order_id: str, body: dict[str, Any]) -> Any:
1377
+ """PUT /designer/production_orders/{id} — re-pick which supplier links the order covers."""
1378
+ return put_json(f"/designer/production_orders/{production_order_id}", body, timeout=120.0)
1379
+
1380
+
1381
+ def designer_confirm_receive_shipping_order(shipping_order_id: str) -> Any:
1382
+ """POST /designer/shipping_orders/{id}/confirm_receive — IN_TRANSIT only."""
1383
+ return post_json(f"/designer/shipping_orders/{shipping_order_id}/confirm_receive", {})
1384
+
1385
+
1386
+ def designer_cancel_shipping_order(shipping_order_id: str) -> Any:
1387
+ """POST /designer/shipping_orders/{id}/cancel — unlinks every production order, then deletes it."""
1388
+ return post_json(f"/designer/shipping_orders/{shipping_order_id}/cancel", {})
@@ -0,0 +1,83 @@
1
+ """Token broker client: how the CLI acts for someone without holding their token.
2
+
3
+ A host that runs the CLI on a user's behalf (the Beeboo agent runner) does not
4
+ put that user's token in the environment, where any process in the sandbox
5
+ could read it. It runs a broker on a unix socket and names it in
6
+ ``MAKEA_TOKEN_BROKER``. Before a command runs, the host's gate has already
7
+ looked at the exact command line and left a one-time ticket for it; the CLI
8
+ sends its own argv and gets back a token that is good for this invocation.
9
+
10
+ A process that did not come through the gate (``python -c``, ``curl``, a CLI
11
+ call wrapped in a script) has no ticket and gets nothing. That is the whole
12
+ point, so there is deliberately no fallback to a cached login in this mode.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import socket
20
+ import sys
21
+
22
+ from makea_cli.errors import AuthError
23
+
24
+ BROKER_ENV = "MAKEA_TOKEN_BROKER"
25
+
26
+ # A broker lives on the same box; anything slower than this is a broker that
27
+ # is not there.
28
+ _TIMEOUT_SECONDS = 10.0
29
+
30
+ # One invocation spends one ticket. Every request the command makes reuses the
31
+ # token the ticket bought.
32
+ _cached: dict[str, str] | None = None
33
+
34
+
35
+ def broker_path() -> str | None:
36
+ path = (os.environ.get(BROKER_ENV) or "").strip()
37
+ return path or None
38
+
39
+
40
+ def invocation_argv() -> list[str]:
41
+ """This process's command line, as the gate saw it (without the program name)."""
42
+ return list(sys.argv[1:])
43
+
44
+
45
+ def exchange(path: str, argv: list[str]) -> dict[str, str]:
46
+ """Trade this command line for ``{"token", "persona"}``, or raise AuthError."""
47
+ request = json.dumps({"argv": argv}).encode("utf-8") + b"\n"
48
+ try:
49
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
50
+ sock.settimeout(_TIMEOUT_SECONDS)
51
+ sock.connect(path)
52
+ sock.sendall(request)
53
+ chunks = []
54
+ while True:
55
+ chunk = sock.recv(65536)
56
+ if not chunk:
57
+ break
58
+ chunks.append(chunk)
59
+ if chunk.endswith(b"\n"):
60
+ break
61
+ except OSError as e:
62
+ raise AuthError(f"token broker unavailable ({e.__class__.__name__})") from e
63
+ try:
64
+ reply = json.loads(b"".join(chunks).decode("utf-8") or "{}")
65
+ except (UnicodeDecodeError, json.JSONDecodeError) as e:
66
+ raise AuthError("token broker sent an unreadable reply") from e
67
+ if not reply.get("ok") or not reply.get("token"):
68
+ raise AuthError(
69
+ str(reply.get("error") or "the token broker refused this command"),
70
+ detail={"broker": True},
71
+ )
72
+ return {"token": str(reply["token"]), "persona": str(reply.get("persona") or "")}
73
+
74
+
75
+ def brokered_token() -> str | None:
76
+ """The token for this invocation when a broker is configured, else None."""
77
+ global _cached
78
+ path = broker_path()
79
+ if not path:
80
+ return None
81
+ if _cached is None:
82
+ _cached = exchange(path, invocation_argv())
83
+ return _cached["token"]
@@ -562,6 +562,197 @@ def register(app: typer.Typer) -> None:
562
562
  body["channel_key"] = channel
563
563
  print_json_or_exit(console, lambda: api_client.designer_add_product_note(product_id, body))
564
564
 
565
+ # --- order lifecycle ---------------------------------------------------------
566
+ #
567
+ # The `approval` grade on each of these is advice to a harness, not a gate in
568
+ # the CLI: a person in a terminal just runs the command. `ask` means an agent
569
+ # must put it in front of someone; `never` means an agent must not run it at
570
+ # all, which is why cancel and delete are their own commands rather than flags
571
+ # on an `ask` one -- the grade is per command, so mixing them would launder a
572
+ # destructive action through a milder grade.
573
+ #
574
+ # Payments and subscriptions are deliberately absent. They exist on the
575
+ # backend; giving them a CLI surface would put a charge one command away from
576
+ # an agent, and no approval grade makes that a good trade.
577
+
578
+ @command(
579
+ app,
580
+ "designer",
581
+ "submit",
582
+ "sampling-order",
583
+ api="POST /designer/sampling_orders/{sampling_order_id}/confirm",
584
+ side_effects=[
585
+ "confirms the sample order to the supplier",
586
+ "starts the supplier's clock on it",
587
+ ],
588
+ approval="ask",
589
+ help=(
590
+ "POST /designer/sampling_orders/{id}/confirm — confirm a sample order so the "
591
+ "supplier starts. Run --dry-run first and read it back to the user."
592
+ ),
593
+ )
594
+ def submit_sampling_order(
595
+ sampling_order_id: str = typer.Argument(..., help="Sampling order id."),
596
+ ) -> None:
597
+ """Confirm a sampling order."""
598
+ print_json_or_exit(
599
+ console, lambda: api_client.designer_confirm_sampling_order(sampling_order_id)
600
+ )
601
+
602
+ @command(
603
+ app,
604
+ "designer",
605
+ "update",
606
+ "sampling-order",
607
+ api="POST /designer/sampling_orders/{sampling_order_id}/cancel",
608
+ side_effects=["cancels the order", "tells the supplier to stop"],
609
+ approval="never",
610
+ help=(
611
+ "POST /designer/sampling_orders/{id}/cancel — cancel a sample order. Graded "
612
+ "`never`: an agent must not do this on its own, a person runs it."
613
+ ),
614
+ )
615
+ def update_sampling_order(
616
+ sampling_order_id: str = typer.Argument(..., help="Sampling order id."),
617
+ cancel: bool = typer.Option(
618
+ ..., "--cancel", help="The only operation here; explicit on purpose."
619
+ ),
620
+ ) -> None:
621
+ """Cancel a sampling order."""
622
+ if not cancel:
623
+ raise typer.BadParameter("--cancel is required")
624
+ print_json_or_exit(
625
+ console, lambda: api_client.designer_cancel_sampling_order(sampling_order_id)
626
+ )
627
+
628
+ @command(
629
+ app,
630
+ "designer",
631
+ "delete",
632
+ "sampling-order",
633
+ api="DELETE /designer/sampling_orders/{sampling_order_id}",
634
+ side_effects=["removes the order row; the backend allows this on DRAFT only"],
635
+ approval="never",
636
+ help=(
637
+ "DELETE /designer/sampling_orders/{id} — delete a DRAFT sample order. The backend "
638
+ "refuses any other status. Graded `never`."
639
+ ),
640
+ )
641
+ def delete_sampling_order(
642
+ sampling_order_id: str = typer.Argument(..., help="Sampling order id (DRAFT)."),
643
+ ) -> None:
644
+ """Delete a draft sampling order."""
645
+ print_json_or_exit(
646
+ console, lambda: api_client.designer_delete_sampling_order(sampling_order_id)
647
+ )
648
+
649
+ @command(
650
+ app,
651
+ "designer",
652
+ "submit",
653
+ "production-order",
654
+ api="POST /designer/production_orders/{production_order_id}/submit",
655
+ side_effects=["hands the bulk order to the supplier", "creates its charges"],
656
+ approval="ask",
657
+ help=(
658
+ "POST /designer/production_orders/{id}/submit — submit a 大货 order. This is the "
659
+ "commitment, so --dry-run and confirm with the user first."
660
+ ),
661
+ )
662
+ def submit_production_order(
663
+ production_order_id: str = typer.Argument(..., help="Production order id."),
664
+ ) -> None:
665
+ """Submit a production order."""
666
+ print_json_or_exit(
667
+ console, lambda: api_client.designer_submit_production_order(production_order_id)
668
+ )
669
+
670
+ @command(
671
+ app,
672
+ "designer",
673
+ "update",
674
+ "production-order",
675
+ api="POST /designer/production_orders/{production_order_id}/complete",
676
+ side_effects=["completing closes the order for both sides"],
677
+ approval="ask",
678
+ help=(
679
+ "POST /designer/production_orders/{id}/complete, or PUT the order to re-pick which "
680
+ "supplier links it covers (--links-file, same body shape as creating it)."
681
+ ),
682
+ )
683
+ def update_production_order(
684
+ production_order_id: str = typer.Argument(..., help="Production order id."),
685
+ complete: bool = typer.Option(False, "--complete", help="Mark the order finished."),
686
+ links_file: Path | None = typer.Option(
687
+ None,
688
+ "--links-file",
689
+ exists=True,
690
+ dir_okay=False,
691
+ help="JSON with selected_links, to change what the order covers.",
692
+ ),
693
+ ) -> None:
694
+ """Complete a production order, or change the links it covers."""
695
+ if complete == (links_file is not None):
696
+ raise typer.BadParameter("pass exactly one of --complete / --links-file")
697
+ if complete:
698
+ run = lambda: api_client.designer_complete_production_order(production_order_id) # noqa: E731
699
+ else:
700
+ body = _json_object(None, links_file, "links-file")
701
+ run = lambda: api_client.designer_update_production_order(production_order_id, body) # noqa: E731
702
+ print_json_or_exit(console, run)
703
+
704
+ @command(
705
+ app,
706
+ "designer",
707
+ "update",
708
+ "shipping-order",
709
+ api="POST /designer/shipping_orders/{shipping_order_id}/confirm_receive",
710
+ side_effects=["marks the shipment received, which closes it"],
711
+ approval="ask",
712
+ help=(
713
+ "POST /designer/shipping_orders/{id}/confirm_receive — say the goods arrived. The "
714
+ "backend accepts this only while the shipment is IN_TRANSIT."
715
+ ),
716
+ )
717
+ def update_shipping_order(
718
+ shipping_order_id: str = typer.Argument(..., help="Shipping order id."),
719
+ confirm_receive: bool = typer.Option(
720
+ ..., "--confirm-receive", help="The only operation here; explicit on purpose."
721
+ ),
722
+ ) -> None:
723
+ """Confirm receipt of a shipment."""
724
+ if not confirm_receive:
725
+ raise typer.BadParameter("--confirm-receive is required")
726
+ print_json_or_exit(
727
+ console,
728
+ lambda: api_client.designer_confirm_receive_shipping_order(shipping_order_id),
729
+ )
730
+
731
+ @command(
732
+ app,
733
+ "designer",
734
+ "delete",
735
+ "shipping-order",
736
+ api="POST /designer/shipping_orders/{shipping_order_id}/cancel",
737
+ side_effects=[
738
+ "unlinks every production order on the shipment",
739
+ "deletes the shipment itself",
740
+ ],
741
+ approval="never",
742
+ help=(
743
+ "POST /designer/shipping_orders/{id}/cancel — the endpoint is called cancel but it "
744
+ "unlinks every production order and removes the shipment, so it is a delete here. "
745
+ "Graded `never`."
746
+ ),
747
+ )
748
+ def delete_shipping_order(
749
+ shipping_order_id: str = typer.Argument(..., help="Shipping order id."),
750
+ ) -> None:
751
+ """Cancel (and remove) a shipping order."""
752
+ print_json_or_exit(
753
+ console, lambda: api_client.designer_cancel_shipping_order(shipping_order_id)
754
+ )
755
+
565
756
  @command(
566
757
  app,
567
758
  "designer",
@@ -626,3 +817,100 @@ def register(app: typer.Typer) -> None:
626
817
  if makea_sourcing:
627
818
  body["makea_sourcing"] = True
628
819
  print_json_or_exit(console, lambda: api_client.designer_send_rfq(body))
820
+
821
+ # --- the sampling journey: confirm the pack, award a quote, pay -------------
822
+
823
+ @command(
824
+ app,
825
+ "designer",
826
+ "submit",
827
+ "tech-pack",
828
+ api="POST /designer/products/{product_id}/technical-specifications/confirm",
829
+ side_effects=[
830
+ "locks the tech pack as the version suppliers quote against",
831
+ "opens the product for sourcing",
832
+ ],
833
+ approval="ask",
834
+ help=(
835
+ "POST /designer/products/{id}/technical-specifications/confirm — confirm the "
836
+ "tech pack, the step before anyone is asked to quote."
837
+ ),
838
+ )
839
+ def submit_tech_pack(
840
+ product_id: str = typer.Argument(..., help="Product id."),
841
+ ) -> None:
842
+ """Confirm a product's tech pack."""
843
+ print_json_or_exit(console, lambda: api_client.designer_confirm_tech_spec(product_id))
844
+
845
+ @command(
846
+ app,
847
+ "designer",
848
+ "create",
849
+ "quote-award",
850
+ api="POST /designer/quotes/group_pay_prepare",
851
+ side_effects=[
852
+ "creates (or reuses) a DRAFT sampling order on each chosen quote",
853
+ "nothing is charged until that order is paid",
854
+ ],
855
+ approval="ask",
856
+ help=(
857
+ "POST /designer/quotes/group_pay_prepare — choose the supplier quote to sample "
858
+ "with. --quote is the supplier link id from `designer list supplier-link`; "
859
+ "repeat --option for the quote's optional extras. Prints the sampling_order_ids "
860
+ "to pay with `designer create checkout --key sampling:<id>`."
861
+ ),
862
+ )
863
+ def create_quote_award(
864
+ product_id: str = typer.Argument(..., help="Product id."),
865
+ quote: str = typer.Option(..., "--quote", help="Supplier link id of the chosen quote."),
866
+ option: list[str] = typer.Option(
867
+ [], "--option", help="Optional extra on the quote to include (repeat)."
868
+ ),
869
+ no_base_sampling: bool = typer.Option(
870
+ False, "--no-base-sampling", help="Leave the quote's base sample out."
871
+ ),
872
+ notes: str | None = typer.Option(None, "--notes", help="Note for the supplier."),
873
+ ) -> None:
874
+ """Award a quote: the sampling order that pays for it."""
875
+ selection: dict[str, Any] = {"product_id": product_id, "link_id": quote}
876
+ if option:
877
+ selection["selected_option_ids"] = list(option)
878
+ if no_base_sampling:
879
+ selection["include_base_sampling"] = False
880
+ if notes:
881
+ selection["notes"] = notes
882
+ print_json_or_exit(
883
+ console, lambda: api_client.designer_award_quotes({"selections": [selection]})
884
+ )
885
+
886
+ @command(
887
+ app,
888
+ "designer",
889
+ "create",
890
+ "checkout",
891
+ api="POST /designer/payments/basket/create-session",
892
+ side_effects=[
893
+ "records the brand's acceptance of the sampling/production terms",
894
+ "creates a Stripe checkout session; nothing is charged until it is paid there",
895
+ ],
896
+ approval="ask",
897
+ help=(
898
+ "POST /designer/payments/basket/create-session — a Stripe checkout link for one "
899
+ "or more charges. --key repeats: sampling:<sampling_order_id>, "
900
+ "production:<production_order_id>:deposit|final, shipping:<id>, misc:<id> "
901
+ "(the `key` of each payable in `designer list payment`). Prints checkout_url."
902
+ ),
903
+ )
904
+ def create_checkout(
905
+ key: list[str] = typer.Option(..., "--key", help="Charge key to pay (repeat)."),
906
+ accept_terms: bool = typer.Option(
907
+ False,
908
+ "--accept-terms",
909
+ help="The brand accepts the sampling/production terms for these charges.",
910
+ ),
911
+ ) -> None:
912
+ """Create a Stripe checkout link for charges."""
913
+ body: dict[str, Any] = {"keys": list(key)}
914
+ if accept_terms:
915
+ body["terms_accepted"] = True
916
+ print_json_or_exit(console, lambda: api_client.designer_create_checkout(body))