rathflow-cli 0.1.2__tar.gz → 0.1.3__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 (36) hide show
  1. {rathflow_cli-0.1.2/rathflow_cli.egg-info → rathflow_cli-0.1.3}/PKG-INFO +5 -5
  2. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/README.md +4 -4
  3. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/pyproject.toml +1 -1
  4. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/__init__.py +1 -1
  5. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/cli.py +4 -1
  6. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/agent.py +44 -1
  7. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/auth_cmd.py +101 -0
  8. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/org.py +16 -0
  9. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/workflow.py +13 -0
  10. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/endpoints.py +20 -1
  11. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/selftest.py +54 -2
  12. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3/rathflow_cli.egg-info}/PKG-INFO +5 -5
  13. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/LICENSE +0 -0
  14. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/__main__.py +0 -0
  15. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/__init__.py +0 -0
  16. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/_common.py +0 -0
  17. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/admin.py +0 -0
  18. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/api_cmd.py +0 -0
  19. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/asset.py +0 -0
  20. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/billing.py +0 -0
  21. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/config_cmd.py +0 -0
  22. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/memory.py +0 -0
  23. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/project.py +0 -0
  24. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/sandbox.py +0 -0
  25. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/commands/session.py +0 -0
  26. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/config.py +0 -0
  27. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/errors.py +0 -0
  28. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/http.py +0 -0
  29. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/output.py +0 -0
  30. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli/state.py +0 -0
  31. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli.egg-info/SOURCES.txt +0 -0
  32. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli.egg-info/dependency_links.txt +0 -0
  33. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli.egg-info/entry_points.txt +0 -0
  34. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli.egg-info/requires.txt +0 -0
  35. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/rathflow_cli.egg-info/top_level.txt +0 -0
  36. {rathflow_cli-0.1.2 → rathflow_cli-0.1.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rathflow-cli
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: RathFlow command line client: sessions, memory, sandboxes, assets and billing over the RathFlow REST API.
5
5
  Author: Rath Team
6
6
  License-Expression: MIT
@@ -75,17 +75,17 @@ The default gateway is `https://rathflow.lynwe.com`. Point it elsewhere with
75
75
 
76
76
  | Group | What it covers |
77
77
  | --- | --- |
78
- | `auth` | log in / out, register, API keys (`keys`, `key-create`, `key-revoke`) |
78
+ | `auth` | log in / out, register, profile (`profile`, `profile-update`, `change-password`, `set-email`, `set-avatar`), API keys |
79
79
  | `config` | local profiles (`list`, `show`, `set`, `use`) |
80
- | `org` | organizations, invitations |
80
+ | `org` | organizations (`list`, `create`, `invite`, `accept`) |
81
81
  | `project` | projects, scope switching, project config, share links |
82
82
  | `session` | sessions, blocks, events |
83
83
  | `memory` | memory entries, search, commit tasks |
84
84
  | `sandbox` | sandboxes, exec, code, files, logs |
85
- | `agent` | agent definitions, versions, prompts, runs |
85
+ | `agent` | agent definitions, versions, prompts, runs, attachments |
86
86
  | `asset` | assets, versions, credentials, publish |
87
87
  | `billing` | subscription, usage, invoices |
88
- | `workflow` | workflows |
88
+ | `workflow` | workflows (`list`, `get`, `create`, `delete`, `counts`) |
89
89
  | `admin` | platform administration (platform admins only) |
90
90
 
91
91
  Two more entry points sit at the root:
@@ -44,17 +44,17 @@ The default gateway is `https://rathflow.lynwe.com`. Point it elsewhere with
44
44
 
45
45
  | Group | What it covers |
46
46
  | --- | --- |
47
- | `auth` | log in / out, register, API keys (`keys`, `key-create`, `key-revoke`) |
47
+ | `auth` | log in / out, register, profile (`profile`, `profile-update`, `change-password`, `set-email`, `set-avatar`), API keys |
48
48
  | `config` | local profiles (`list`, `show`, `set`, `use`) |
49
- | `org` | organizations, invitations |
49
+ | `org` | organizations (`list`, `create`, `invite`, `accept`) |
50
50
  | `project` | projects, scope switching, project config, share links |
51
51
  | `session` | sessions, blocks, events |
52
52
  | `memory` | memory entries, search, commit tasks |
53
53
  | `sandbox` | sandboxes, exec, code, files, logs |
54
- | `agent` | agent definitions, versions, prompts, runs |
54
+ | `agent` | agent definitions, versions, prompts, runs, attachments |
55
55
  | `asset` | assets, versions, credentials, publish |
56
56
  | `billing` | subscription, usage, invoices |
57
- | `workflow` | workflows |
57
+ | `workflow` | workflows (`list`, `get`, `create`, `delete`, `counts`) |
58
58
  | `admin` | platform administration (platform admins only) |
59
59
 
60
60
  Two more entry points sit at the root:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rathflow-cli"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "RathFlow command line client: sessions, memory, sandboxes, assets and billing over the RathFlow REST API."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -3,4 +3,4 @@
3
3
  安装:``uv tool install rathflow-cli``(或 ``pip install rathflow-cli``)。
4
4
  """
5
5
 
6
- __version__ = "0.1.2"
6
+ __version__ = "0.1.3"
@@ -101,7 +101,10 @@ def _value_opts() -> set[str]:
101
101
  for param in getattr(cmd, "params", []):
102
102
  if getattr(param, "is_flag", False) or getattr(param, "count", False):
103
103
  continue
104
- found.update(getattr(param, "opts", []))
104
+ # 只认真正的选项(以 "-" 开头)。位置参数的 opts 是它自己的名字
105
+ # (例如 `auth profile` 的 `profile`),把它当吃值的选项会让
106
+ # `rathflow auth profile --json` 里的 --json 不被前移而报错。
107
+ found.update(o for o in getattr(param, "opts", []) if o.startswith("-"))
105
108
  for sub in getattr(cmd, "commands", {}).values():
106
109
  found |= walk(sub)
107
110
  return found
@@ -1,7 +1,9 @@
1
- """agent:Agent 定义、版本、运行与 Agent 目录。"""
1
+ """agent:Agent 定义、版本、运行、Agent 目录与附件。"""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import base64
6
+
5
7
  import typer
6
8
 
7
9
  from .. import output
@@ -271,6 +273,47 @@ def messages(
271
273
  )
272
274
 
273
275
 
276
+ @app.command("attachment")
277
+ def attachment(
278
+ attachment_id: str = typer.Argument(..., help="sha256:<hex>,取自事件里的 attachmentRef"),
279
+ out: str = typer.Option(None, "--out", "-o", help="写文件;缺省写 stdout"),
280
+ ) -> None:
281
+ """读附件字节(流式,分片直写,不整包缓冲)。"""
282
+ st = current()
283
+ fh = None
284
+ try:
285
+ if out:
286
+ fh = open(out, "wb")
287
+ for chunk in st.client().stream(
288
+ "agent.ReadAttachment", path_params={"attachment_id": attachment_id}
289
+ ):
290
+ if st.json_out:
291
+ output.emit_json(chunk)
292
+ continue
293
+ data = _bytes(chunk.get("data"))
294
+ if not data:
295
+ continue
296
+ if fh:
297
+ fh.write(data)
298
+ else:
299
+ output.write_bytes(data)
300
+ finally:
301
+ if fh:
302
+ fh.close()
303
+
304
+
305
+ def _bytes(value) -> bytes:
306
+ """protojson 把 bytes 编成 base64;解失败就当纯文本用。"""
307
+ if not value:
308
+ return b""
309
+ if isinstance(value, bytes):
310
+ return value
311
+ try:
312
+ return base64.b64decode(value, validate=True)
313
+ except Exception:
314
+ return str(value).encode()
315
+
316
+
274
317
  def _text_of(text: str | None, file: str | None) -> str | None:
275
318
  """config_json 是自由 JSON 文本:字面量或文件(- 读 stdin)。"""
276
319
  if text and file:
@@ -2,9 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import base64
6
+ import mimetypes
7
+ import sys
8
+
5
9
  import typer
6
10
 
7
11
  from .. import config, output
12
+ from ..errors import UsageError
8
13
  from ..state import current
9
14
 
10
15
  app = typer.Typer(no_args_is_help=True, help="登录 / 登出 / 注册 / 身份")
@@ -123,6 +128,102 @@ def key_revoke(
123
128
  typer.echo(f"已吊销 {key_id}")
124
129
 
125
130
 
131
+ # ------------------------------------------------------------------ 个人资料
132
+
133
+ _AVATAR_MEDIA_TYPES = {"image/png", "image/jpeg", "image/webp", "image/gif"}
134
+
135
+
136
+ @app.command("profile")
137
+ def profile() -> None:
138
+ """看当前账号资料(头像以 base64 内联,这里只显示「有/类型」,字节走 --json)。"""
139
+ st = current()
140
+ payload = st.client().call("auth.GetProfile")
141
+ if st.json_out:
142
+ output.emit_json(payload)
143
+ return
144
+ view = {k: v for k, v in payload.items() if k != "avatarBytes"}
145
+ if payload.get("avatarBytes"):
146
+ view["avatar"] = payload.get("avatarMediaType") or "有(--json 取字节)"
147
+ output.emit_object(view, json_out=False)
148
+
149
+
150
+ @app.command("profile-update")
151
+ def profile_update(
152
+ display_name: str = typer.Option(None, "--display-name", help="显示名"),
153
+ username: str = typer.Option(None, "--username"),
154
+ phone: str = typer.Option(None, "--phone"),
155
+ ) -> None:
156
+ """改资料;只发显式给出的字段(都不给 = 用法错,避免空 PATCH)。"""
157
+ st = current()
158
+ body: dict = {}
159
+ if display_name is not None:
160
+ body["displayName"] = display_name
161
+ if username is not None:
162
+ body["username"] = username
163
+ if phone is not None:
164
+ body["phone"] = phone
165
+ if not body:
166
+ raise UsageError("至少给一个:--display-name / --username / --phone")
167
+ output.emit_object(st.client().call("auth.UpdateProfile", body=body), json_out=st.json_out)
168
+
169
+
170
+ @app.command("change-password")
171
+ def change_password(
172
+ current_password: str = typer.Option(None, "--current-password", help="缺省交互输入,不进 shell 历史"),
173
+ new_password: str = typer.Option(None, "--new-password", help="缺省交互输入;>=8 位且含大小写"),
174
+ ) -> None:
175
+ """改密码(旧密码证明是本人)。"""
176
+ st = current()
177
+ current_password = current_password or typer.prompt("当前密码", hide_input=True)
178
+ new_password = new_password or typer.prompt("新密码", hide_input=True, confirmation_prompt=True)
179
+ st.client().call(
180
+ "auth.ChangePassword",
181
+ body={"currentPassword": current_password, "newPassword": new_password},
182
+ )
183
+ typer.echo("密码已更新")
184
+
185
+
186
+ @app.command("set-email")
187
+ def set_email(
188
+ new_email: str = typer.Option(..., "--email", "-e", help="新登录邮箱"),
189
+ current_password: str = typer.Option(None, "--current-password", help="缺省交互输入"),
190
+ ) -> None:
191
+ """换登录邮箱(要当前密码,防止被盗会话直接改)。"""
192
+ st = current()
193
+ current_password = current_password or typer.prompt("当前密码", hide_input=True)
194
+ payload = st.client().call(
195
+ "auth.UpdateEmail",
196
+ body={"newEmail": new_email, "currentPassword": current_password},
197
+ )
198
+ output.emit_object(payload, json_out=st.json_out)
199
+
200
+
201
+ @app.command("set-avatar")
202
+ def set_avatar(
203
+ file: str = typer.Option(None, "--file", "-f", help="图片文件;传 - 读 stdin"),
204
+ media_type: str = typer.Option(None, "--media-type", help="缺省按扩展名猜"),
205
+ clear: bool = typer.Option(False, "--clear", help="清除头像"),
206
+ ) -> None:
207
+ """换/清头像(base64 内联,服务端限 64KB 方图)。"""
208
+ st = current()
209
+ if clear and file:
210
+ raise UsageError("--clear 与 --file 互斥")
211
+ if not clear and not file:
212
+ raise UsageError("给 --file <图片>,或用 --clear 清除头像")
213
+ body: dict = {}
214
+ if clear:
215
+ body = {"data": "", "mediaType": ""}
216
+ else:
217
+ raw = sys.stdin.buffer.read() if file == "-" else open(file, "rb").read()
218
+ mt = media_type or (mimetypes.guess_type(file)[0] if file != "-" else None)
219
+ if mt not in _AVATAR_MEDIA_TYPES:
220
+ raise UsageError(
221
+ f"媒体类型 {mt or '(未知)'} 不在白名单:{', '.join(sorted(_AVATAR_MEDIA_TYPES))}(用 --media-type 指定)"
222
+ )
223
+ body = {"data": base64.b64encode(raw).decode(), "mediaType": mt}
224
+ output.emit_object(st.client().call("auth.UpdateAvatar", body=body), json_out=st.json_out)
225
+
226
+
126
227
  def whoami() -> None:
127
228
  """显示登录身份 + 作用域 + 可访问项目(后者顺带证明令牌有效)。"""
128
229
  st = current()
@@ -30,6 +30,22 @@ def list_() -> None:
30
30
  output.emit_items(payload, items_key="orgs", columns=_COLUMNS, json_out=st.json_out)
31
31
 
32
32
 
33
+ @app.command("create")
34
+ def create(
35
+ name: str = typer.Option(..., "--name", "-n", help="展示名(1..60 字符;slug 服务端派生)"),
36
+ ) -> None:
37
+ """新建组织(自己是 OWNER,且会附带一个默认 project)。"""
38
+ st = current()
39
+ payload = st.client().call("tenant.CreateOrg", body={"name": name})
40
+ if st.json_out:
41
+ output.emit_json(payload)
42
+ return
43
+ org = payload.get("org") or {}
44
+ typer.echo(f"已创建 {org.get('orgId') or '?'} {org.get('name') or name}")
45
+ if payload.get("defaultProjectId"):
46
+ typer.echo(f"切过去:rathflow project use {payload['defaultProjectId']}")
47
+
48
+
33
49
  @app.command("invite")
34
50
  def invite(
35
51
  org_id: str = typer.Argument(...),
@@ -44,6 +44,19 @@ def get(workflow_id: str = typer.Argument(...)) -> None:
44
44
  output.emit_object(payload, json_out=st.json_out)
45
45
 
46
46
 
47
+ @app.command("counts")
48
+ def counts() -> None:
49
+ """每个 project 的存活工作流数(服务端不返回 0 的行 = 没有)。"""
50
+ st = current()
51
+ payload = st.client().call("workflow.CountWorkflowsByProject")
52
+ output.emit_items(
53
+ payload,
54
+ items_key="counts",
55
+ columns=[("PROJECT ID", "projectId"), ("数量", "count")],
56
+ json_out=st.json_out,
57
+ )
58
+
59
+
47
60
  @app.command("create")
48
61
  def create(
49
62
  title: str = typer.Option(None, "--title", "-t"),
@@ -21,6 +21,11 @@ ENDPOINTS: dict[str, tuple[str, str]] = {
21
21
  "auth.Login": ("POST", "/api/v1/auth/login"),
22
22
  "auth.Logout": ("POST", "/api/v1/auth/logout"),
23
23
  "auth.Refresh": ("POST", "/api/v1/auth/refresh"),
24
+ "auth.ChangePassword": ("POST", "/api/v1/profile/password"),
25
+ "auth.GetProfile": ("GET", "/api/v1/profile"),
26
+ "auth.UpdateAvatar": ("PUT", "/api/v1/profile/avatar"),
27
+ "auth.UpdateEmail": ("POST", "/api/v1/profile/email"),
28
+ "auth.UpdateProfile": ("PATCH", "/api/v1/profile"),
24
29
  "auth.Register": ("POST", "/api/v1/auth/register"),
25
30
  "auth.RevokeAPIKey": ("DELETE", "/api/v1/api-keys/{key_id}"),
26
31
  # ---- tenant:分享链接(能力凭证,明文只在创建/轮换时出现一次)----
@@ -30,6 +35,7 @@ ENDPOINTS: dict[str, tuple[str, str]] = {
30
35
  "tenant.UpdateProjectShare": ("PATCH", "/api/v1/projects/{project_id}/share"),
31
36
  # ---- tenant:org 成员身份 ----
32
37
  "tenant.AcceptOrgInvitation": ("POST", "/api/v1/orgs/join"),
38
+ "tenant.CreateOrg": ("POST", "/api/v1/orgs"),
33
39
  "tenant.CreateOrgInvitation": ("POST", "/api/v1/orgs/{org_id}/invitations"),
34
40
  "tenant.ListMyOrgs": ("GET", "/api/v1/orgs"),
35
41
  # ---- tenant:project ----
@@ -56,6 +62,7 @@ ENDPOINTS: dict[str, tuple[str, str]] = {
56
62
  "session.ShareBlock": ("POST", "/api/v1/blocks/{block_id}/shares"),
57
63
  "session.StreamSessionEvents": ("GET", "/api/v1/sessions/{session_id}/events:stream"),
58
64
  # ---- workflow ----
65
+ "workflow.CountWorkflowsByProject": ("GET", "/api/v1/workflows:counts"),
59
66
  "workflow.CreateWorkflow": ("POST", "/api/v1/workflows"),
60
67
  "workflow.DeleteWorkflow": ("DELETE", "/api/v1/workflows/{workflow_id}"),
61
68
  "workflow.GetWorkflow": ("GET", "/api/v1/workflows/{workflow_id}"),
@@ -63,6 +70,7 @@ ENDPOINTS: dict[str, tuple[str, str]] = {
63
70
  "workflow.UpdateWorkflow": ("PATCH", "/api/v1/workflows/{workflow_id}"),
64
71
  # ---- agent ----
65
72
  "agent.CreateAgentDef": ("POST", "/api/v1/agents"),
73
+ "agent.ReadAttachment": ("GET", "/api/v1/attachments/{attachment_id}"),
66
74
  "agent.DeleteAgentDef": ("DELETE", "/api/v1/agents/{agent_def_id}"),
67
75
  "agent.GetAgentDef": ("GET", "/api/v1/agents/{agent_def_id}"),
68
76
  "agent.GetAgentDefVersion": ("GET", "/api/v1/agents/{agent_def_id}/versions/{version}"),
@@ -155,6 +163,7 @@ MULTI_PARAMS: dict[str, frozenset[str]] = {
155
163
  # 走 call() 会拿到逐行 JSON 的未解析正文。
156
164
  STREAMING: frozenset[str] = frozenset(
157
165
  {
166
+ "agent.ReadAttachment",
158
167
  "sandbox.RunCommand",
159
168
  "sandbox.RunCode",
160
169
  "sandbox.ReadFile",
@@ -197,7 +206,17 @@ def render_path(key: str, params: dict) -> str:
197
206
  raise UsageError(f"{key} 需要路径参数 --path {name}=<值>")
198
207
  text = str(value)
199
208
  if name in multi:
200
- return "/".join(quote(seg, safe="") for seg in text.split("/"))
209
+ segments = text.split("/")
210
+ # `.` / `..` / 空段会被 HTTP 客户端按 URL 语义归一化(`a/b/../c` → `a/c`,
211
+ # `//x` → `/x`),于是实际请求会跑到别的端点上,还顺手绕过上面那层
212
+ # 「首段在域内」的检查。逐段 encode 拦不住它们(`.` 是 unreserved,
213
+ # quote 不会动),所以在这里直接拒掉,要求相对、干净的路径。
214
+ for seg in segments:
215
+ if seg in ("", ".", ".."):
216
+ raise UsageError(
217
+ f"{name} 不能含空段或 '.'/'..':{text!r}(要相对路径,如 memories/notes/a.md)"
218
+ )
219
+ return "/".join(quote(seg, safe="") for seg in segments)
201
220
  return quote(text, safe="")
202
221
 
203
222
  return _TEMPLATE_RE.sub(repl, template)
@@ -1,12 +1,14 @@
1
1
  """轻量自检(不联服务端):``python -m rathflow_cli.selftest``
2
2
 
3
- 查两件事,都是「装得上但会打错 URL」的那类毛病:
3
+ 查三件事,都是「装得上但会打错 URL」的那类毛病:
4
4
 
5
5
  1. **命令实现里不写 URL 字面量** —— 所有请求必须走 ``endpoints.py`` 的端点 key;
6
6
  2. **端点表与生成物一致** —— 生成物 ``endpoints.ts`` 由上游 proto 生成,本包的
7
7
  ``endpoints.py`` 手工同步;两者漂移就会打错 URL。生成物在本仓库中不存在
8
8
  (它是上游 monorepo 的产物),此时这一项跳过,可用环境变量
9
9
  ``RATHFLOW_ENDPOINTS_TS`` 指向它来启用比对。
10
+ 3. **多段路径参数拦住点段** —— ``..`` 会被 HTTP 客户端按 URL 语义归一化,
11
+ 把请求打到别的端点上(还会绕过命令自己的域检查)。
10
12
  """
11
13
 
12
14
  from __future__ import annotations
@@ -16,6 +18,8 @@ import os
16
18
  import sys
17
19
  from pathlib import Path
18
20
 
21
+ from .errors import UsageError
22
+
19
23
  from . import endpoints
20
24
 
21
25
  _COMMANDS = Path(__file__).resolve().parent / "commands"
@@ -46,7 +50,12 @@ _URL_LITERAL = re.compile(r'"(?:/api/|/admin/api/)')
46
50
 
47
51
  def main() -> int:
48
52
  generated_problems, compared = _check_generated()
49
- problems = generated_problems + _check_no_url_literals()
53
+ problems = (
54
+ generated_problems
55
+ + _check_no_url_literals()
56
+ + _check_path_guard()
57
+ + _check_hoist_flags()
58
+ )
50
59
  if problems:
51
60
  print(f"自检未通过({len(problems)} 项):", file=sys.stderr)
52
61
  for line in problems:
@@ -112,6 +121,49 @@ def _check_no_url_literals() -> list[str]:
112
121
  return problems
113
122
 
114
123
 
124
+ def _check_hoist_flags() -> list[str]:
125
+ """全局旗标写在子命令后面也必须能用(脚本里最常见的写法)。"""
126
+ from . import cli
127
+
128
+ problems = []
129
+ for argv in (
130
+ ["rathflow", "auth", "profile", "--json"],
131
+ ["rathflow", "project", "list", "--json"],
132
+ ["rathflow", "config", "use", "default", "--json"],
133
+ ["rathflow", "session", "get", "S1", "--json"],
134
+ ):
135
+ hoisted = cli._hoist_globals(list(argv))
136
+ if hoisted[1:2] != ["--json"]:
137
+ problems.append(f"{' '.join(argv)}: 末尾 --json 未被前移(会报 no such option)")
138
+ return problems
139
+
140
+
141
+ def _check_path_guard() -> list[str]:
142
+ """多段路径里的 ``.``/``..``/空段必须被拒,正常路径仍要能渲染。"""
143
+ cases = {"memory.Read": "memory_path", "sandbox.ReadFile": "path"}
144
+ bad_inputs = ("a/../b", "../x", "/abs", "a//b", "a/./b")
145
+ problems = []
146
+ for key, name in cases.items():
147
+ others = {p: "X1" for p in endpoints.path_params(key) if p != name}
148
+ for bad in bad_inputs:
149
+ try:
150
+ rendered = endpoints.render_path(key, {name: bad, **others})
151
+ except UsageError:
152
+ continue
153
+ except Exception as exc: # noqa: BLE001 - 自检要报「类型不对」
154
+ problems.append(f"{key}: {name}={bad!r} 抛了 {type(exc).__name__},应抛 UsageError")
155
+ else:
156
+ problems.append(
157
+ f"{key}: {name}={bad!r} 未被拒绝,会渲染成 {rendered}(URL 归一化后打到别的端点)"
158
+ )
159
+ good = endpoints.render_path(key, {name: "a/b c.md", **others})
160
+ if not good.endswith("a/b%20c.md"):
161
+ problems.append(f"{key}: 正常路径渲染异常 {good}")
162
+ return problems
163
+
164
+
165
+
166
+
115
167
  def _names(raw: str) -> set[str]:
116
168
  return set(re.findall(r'"([^"]+)"', raw))
117
169
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rathflow-cli
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: RathFlow command line client: sessions, memory, sandboxes, assets and billing over the RathFlow REST API.
5
5
  Author: Rath Team
6
6
  License-Expression: MIT
@@ -75,17 +75,17 @@ The default gateway is `https://rathflow.lynwe.com`. Point it elsewhere with
75
75
 
76
76
  | Group | What it covers |
77
77
  | --- | --- |
78
- | `auth` | log in / out, register, API keys (`keys`, `key-create`, `key-revoke`) |
78
+ | `auth` | log in / out, register, profile (`profile`, `profile-update`, `change-password`, `set-email`, `set-avatar`), API keys |
79
79
  | `config` | local profiles (`list`, `show`, `set`, `use`) |
80
- | `org` | organizations, invitations |
80
+ | `org` | organizations (`list`, `create`, `invite`, `accept`) |
81
81
  | `project` | projects, scope switching, project config, share links |
82
82
  | `session` | sessions, blocks, events |
83
83
  | `memory` | memory entries, search, commit tasks |
84
84
  | `sandbox` | sandboxes, exec, code, files, logs |
85
- | `agent` | agent definitions, versions, prompts, runs |
85
+ | `agent` | agent definitions, versions, prompts, runs, attachments |
86
86
  | `asset` | assets, versions, credentials, publish |
87
87
  | `billing` | subscription, usage, invoices |
88
- | `workflow` | workflows |
88
+ | `workflow` | workflows (`list`, `get`, `create`, `delete`, `counts`) |
89
89
  | `admin` | platform administration (platform admins only) |
90
90
 
91
91
  Two more entry points sit at the root:
File without changes
File without changes