rathflow-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ """billing:订阅、用量与发票(只读)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from .. import output
8
+ from ..commands._common import PAGE_SIZE_OPT, PAGE_TOKEN_OPT, pagination
9
+ from ..state import current
10
+
11
+ app = typer.Typer(no_args_is_help=True, help="订阅 / 用量 / 发票")
12
+
13
+
14
+ @app.command("subscription")
15
+ def subscription(
16
+ project_id: str = typer.Option(None, "--project-id", help="缺省用当前作用域项目"),
17
+ ) -> None:
18
+ """当前订阅。"""
19
+ st = current()
20
+ payload = st.client().call(
21
+ "billing.GetSubscription", query={"project_id": project_id or st.project}
22
+ )
23
+ output.emit_object(payload, json_out=st.json_out)
24
+
25
+
26
+ @app.command("usage")
27
+ def usage(
28
+ project_id: str = typer.Option(None, "--project-id"),
29
+ start: str = typer.Option(None, "--start", help="RFC3339,如 2026-09-01T00:00:00Z"),
30
+ end: str = typer.Option(None, "--end", help="RFC3339"),
31
+ metrics: str = typer.Option(None, "--metrics", help="逗号分隔的指标名"),
32
+ ) -> None:
33
+ """用量汇总。"""
34
+ st = current()
35
+ query = {
36
+ "project_id": project_id or st.project,
37
+ "start_time": start,
38
+ "end_time": end,
39
+ "metrics": metrics,
40
+ }
41
+ payload = st.client().call("billing.GetUsage", query=query)
42
+ output.emit_table(
43
+ payload.get("usages") or [],
44
+ [("指标", "metric"), ("用量", "units")],
45
+ json_out=st.json_out,
46
+ )
47
+
48
+
49
+ @app.command("invoices")
50
+ def invoices(
51
+ project_id: str = typer.Option(None, "--project-id"),
52
+ page_size: int = PAGE_SIZE_OPT,
53
+ page_token: str = PAGE_TOKEN_OPT,
54
+ ) -> None:
55
+ """发票列表。"""
56
+ st = current()
57
+ query = pagination(page_size or 20, page_token, {"project_id": project_id or st.project})
58
+ payload = st.client().call("billing.ListInvoices", query=query)
59
+ output.emit_items(
60
+ payload,
61
+ items_key="invoices",
62
+ columns=[
63
+ ("INVOICE ID", "invoiceId"),
64
+ ("编号", "number"),
65
+ ("周期", "periodStart"),
66
+ ("金额", "amountMinor"),
67
+ ("币种", "currency"),
68
+ ("状态", "status"),
69
+ ],
70
+ json_out=st.json_out,
71
+ )
@@ -0,0 +1,98 @@
1
+ """config:本地配置档(base_url / project / 令牌存放)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from .. import config, output
8
+ from ..errors import UsageError
9
+ from ..state import current
10
+
11
+ app = typer.Typer(no_args_is_help=True, help="本地配置档管理")
12
+
13
+ _KEYS = ("base_url", "project")
14
+
15
+
16
+ @app.command("list")
17
+ def list_() -> None:
18
+ """列出全部配置档。"""
19
+ st = current()
20
+ profiles = st.data.get("profiles") or {}
21
+ rows = []
22
+ for name, prof in profiles.items():
23
+ rows.append(
24
+ {
25
+ "name": name,
26
+ "current": "*" if name == st.profile_name else "",
27
+ "base_url": prof.get("base_url") or config.DEFAULT_BASE_URL,
28
+ "project": prof.get("project") or "",
29
+ "email": (prof.get("user") or {}).get("email", ""),
30
+ "token": config.mask(prof.get("access_token")),
31
+ }
32
+ )
33
+ output.emit_table(
34
+ rows,
35
+ [
36
+ ("", "current"),
37
+ ("配置档", "name"),
38
+ ("地址", "base_url"),
39
+ ("项目", "project"),
40
+ ("邮箱", "email"),
41
+ ("令牌", "token"),
42
+ ],
43
+ json_out=st.json_out,
44
+ )
45
+
46
+
47
+ @app.command("show")
48
+ def show() -> None:
49
+ """显示当前生效配置(flag/env 覆盖后的结果)。"""
50
+ st = current()
51
+ output.emit_object(
52
+ {
53
+ "profile": st.profile_name,
54
+ "base_url": st.base_url,
55
+ "project": st.project or "(未设置)",
56
+ "token": config.mask(st.profile.get("access_token")),
57
+ "config_file": str(config.config_path()),
58
+ "env": {
59
+ config.ENV_BASE_URL: st.base_url,
60
+ config.ENV_PROJECT: st.project,
61
+ config.ENV_TOKEN: config.mask(st.token_override),
62
+ },
63
+ },
64
+ json_out=st.json_out,
65
+ )
66
+
67
+
68
+ @app.command("set")
69
+ def set_(
70
+ key: str = typer.Argument(..., help="base_url | project"),
71
+ value: str = typer.Argument(..., help="值;project 传 - 表示清空"),
72
+ ) -> None:
73
+ """写当前配置档的一项。"""
74
+ st = current()
75
+ if key not in _KEYS:
76
+ raise UsageError(f"可设的键:{', '.join(_KEYS)}")
77
+ prof = st.profile
78
+ if key == "project" and value == "-":
79
+ prof.pop("project", None)
80
+ # 清掉登录时记下的默认项目,否则下次仍会回填
81
+ prof.pop("default_project_id", None)
82
+ typer.echo("已清空 project")
83
+ else:
84
+ prof[key] = value
85
+ typer.echo(f"{key} = {value}")
86
+ st.data["profiles"][st.profile_name] = prof
87
+ config.save(st.data)
88
+
89
+
90
+ @app.command("use")
91
+ def use(profile: str = typer.Argument(..., help="配置档名;不存在则新建")) -> None:
92
+ """切换当前配置档。"""
93
+ st = current()
94
+ if profile not in (st.data.get("profiles") or {}):
95
+ st.data.setdefault("profiles", {})[profile] = {}
96
+ st.data["current"] = profile
97
+ config.save(st.data)
98
+ typer.echo(f"已切到配置档 {profile}(首次使用需在该档下 auth login)")
@@ -0,0 +1,275 @@
1
+ """memory:记忆文件读写、检索、树与 commit 任务。
2
+
3
+ 路径规律(engine-memory/internal/engine/path.go):`memory_path` 的**首段**必须是
4
+ `memories`(项目私有)或 `resources`(共享资源)之一,其余自由。路径是 `{path=**}`
5
+ 多段通配,逐段 encode 由 endpoints.py 负责。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import sys
12
+
13
+ import typer
14
+
15
+ from .. import output
16
+ from ..commands._common import (
17
+ BODY_FILE_OPT,
18
+ BODY_OPT,
19
+ PAGE_SIZE_OPT,
20
+ PAGE_TOKEN_OPT,
21
+ body_of,
22
+ pagination,
23
+ )
24
+ from ..errors import UsageError
25
+ from ..state import current
26
+
27
+ app = typer.Typer(no_args_is_help=True, help="记忆库 / 任务")
28
+
29
+ ROOTS = ("memories", "resources")
30
+ PATH_ARG = typer.Argument(..., help=f"记忆路径,首段须是 {'/'.join(ROOTS)}(如 memories/notes/a.md)")
31
+ FILE_ARG = typer.Argument(None, help="内容文件;缺省或 - 读 stdin")
32
+
33
+
34
+ @app.command("list")
35
+ def list_(
36
+ prefix: str = typer.Option(None, "--prefix", help="路径前缀,如 memories/notes"),
37
+ recursive: bool = typer.Option(False, "--recursive", "-r"),
38
+ page_size: int = PAGE_SIZE_OPT,
39
+ page_token: str = PAGE_TOKEN_OPT,
40
+ ) -> None:
41
+ """列记忆条目(当前缀为空时服务端通常返回空,建议 --prefix memories)。"""
42
+ st = current()
43
+ query = pagination(
44
+ page_size or 100,
45
+ page_token,
46
+ {"prefix": _checked(prefix, allow_empty=True), "recursive": recursive or None},
47
+ )
48
+ payload = st.client().call("memory.List", query=query)
49
+ output.emit_items(
50
+ payload,
51
+ items_key="entries",
52
+ columns=[("路径", "memoryPath"), ("大小", "sizeBytes"), ("更新时间", "updatedAt")],
53
+ json_out=st.json_out,
54
+ )
55
+
56
+
57
+ @app.command("read")
58
+ def read(
59
+ memory_path: str = PATH_ARG,
60
+ level: str = typer.Option(None, "--level", help="读层级(ReadLevel 枚举,如 L0/L1)"),
61
+ ) -> None:
62
+ """读记忆内容(原始字节写 stdout,可直接重定向)。"""
63
+ st = current()
64
+ query = {"level": level} if level else None
65
+ payload = st.client().call(
66
+ "memory.Read", path_params={"memory_path": _checked(memory_path)}, query=query
67
+ )
68
+ if st.json_out:
69
+ output.emit_json(payload)
70
+ return
71
+ content = payload.get("content")
72
+ if content is None:
73
+ output.emit_object(payload, json_out=False)
74
+ return
75
+ output.write_bytes(_bytes(content))
76
+
77
+
78
+ @app.command("write")
79
+ def write(
80
+ memory_path: str = PATH_ARG,
81
+ file: str = FILE_ARG,
82
+ content_type: str = typer.Option(None, "--content-type", help="如 text/markdown"),
83
+ ttl: int = typer.Option(None, "--ttl", help="存活秒数"),
84
+ ) -> None:
85
+ """写记忆内容(`-` 或省略 = 读 stdin;content 是 bytes,编码由 CLI 负责)。"""
86
+ st = current()
87
+ data = _read(file)
88
+ body = {"content": base64.b64encode(data).decode()}
89
+ if content_type:
90
+ body["contentType"] = content_type
91
+ if ttl is not None:
92
+ body["ttlSeconds"] = ttl
93
+ payload = st.client().call(
94
+ "memory.Write", path_params={"memory_path": _checked(memory_path)}, body=body
95
+ )
96
+ if st.json_out:
97
+ output.emit_json(payload)
98
+ return
99
+ written = payload.get("bytesWritten", len(data))
100
+ typer.echo(f"已写入 {memory_path}({written} 字节)")
101
+
102
+
103
+ @app.command("search")
104
+ def search(
105
+ query: str = typer.Argument(..., help="语义检索词"),
106
+ top_k: int = typer.Option(None, "--top-k", help="返回条数(服务端要求 > 0)"),
107
+ min_score: float = typer.Option(None, "--min-score"),
108
+ scope: str = typer.Option(None, "--scope", help="限定路径前缀"),
109
+ mode: str = typer.Option(None, "--mode", help="SearchMode 枚举"),
110
+ paths: bool = typer.Option(False, "--paths", help="只列命中路径(喂给 xargs)"),
111
+ body: str = BODY_OPT,
112
+ body_file: str = BODY_FILE_OPT,
113
+ ) -> None:
114
+ """语义检索(流式:命中逐条到,不必等全量)。"""
115
+ st = current()
116
+ payload_body = body_of(body, body_file) or {}
117
+ payload_body.setdefault("query", query)
118
+ if top_k is not None:
119
+ payload_body.setdefault("topK", top_k)
120
+ if min_score is not None:
121
+ payload_body.setdefault("minScore", min_score)
122
+ if scope:
123
+ payload_body.setdefault("scope", scope)
124
+ if mode:
125
+ payload_body.setdefault("mode", mode)
126
+ rows = []
127
+ empty = True
128
+ for hit in st.client().stream("memory.Search", body=payload_body):
129
+ empty = False
130
+ if st.json_out:
131
+ output.emit_json(hit)
132
+ continue
133
+ if paths:
134
+ typer.echo(hit.get("memoryPath") or "")
135
+ continue
136
+ rows.append(hit)
137
+ if not empty and rows and not st.json_out:
138
+ output.emit_table(
139
+ rows,
140
+ [("路径", "memoryPath"), ("得分", "score"), ("片段", "snippet")],
141
+ json_out=False,
142
+ )
143
+ elif empty and not st.json_out and not paths:
144
+ typer.echo("(无命中)")
145
+
146
+
147
+ @app.command("tree")
148
+ def tree(
149
+ root: str = typer.Option(None, "--root", help=f"子树根,如 {' 或 '.join(ROOTS)}"),
150
+ depth: int = typer.Option(None, "--depth", help="深度上限"),
151
+ ) -> None:
152
+ """目录树。"""
153
+ st = current()
154
+ body = {k: v for k, v in {"root": root, "depthLimit": depth}.items() if v is not None}
155
+ payload = st.client().call("memory.Tree", body=body)
156
+ if st.json_out:
157
+ output.emit_json(payload)
158
+ return
159
+ _print_tree(payload.get("nodes") or [])
160
+
161
+
162
+ @app.command("commit")
163
+ def commit(
164
+ session_id: str = typer.Option(..., "--session", "-s", help="源会话 id"),
165
+ note: str = typer.Option(None, "--note", help="备注(写进提交说明)"),
166
+ ) -> None:
167
+ """把会话提交成记忆(异步,返回 taskId)。"""
168
+ st = current()
169
+ body = {"sessionId": session_id}
170
+ if note:
171
+ body["note"] = note
172
+ payload = st.client().call("memory.CommitSession", body=body)
173
+ if st.json_out:
174
+ output.emit_json(payload)
175
+ return
176
+ task_id = payload.get("taskId")
177
+ typer.echo(f"任务 {task_id};进度:rathflow memory watch-task {task_id}")
178
+
179
+
180
+ @app.command("tasks")
181
+ def tasks(
182
+ session_id: str = typer.Option(None, "--session", "-s", help="只看某会话的任务"),
183
+ page_size: int = PAGE_SIZE_OPT,
184
+ page_token: str = PAGE_TOKEN_OPT,
185
+ ) -> None:
186
+ """列 commit/索引任务。"""
187
+ st = current()
188
+ query = pagination(page_size or 50, page_token, {"session_id": session_id})
189
+ payload = st.client().call("memory.ListTasks", query=query)
190
+ output.emit_items(
191
+ payload,
192
+ items_key="tasks",
193
+ columns=[("TASK ID", "taskId"), ("状态", "status"), ("创建", "createdAt"), ("完成", "finishedAt")],
194
+ json_out=st.json_out,
195
+ )
196
+
197
+
198
+ @app.command("task")
199
+ def task(task_id: str = typer.Argument(...)) -> None:
200
+ """看单个任务。"""
201
+ st = current()
202
+ payload = st.client().call("memory.GetTask", path_params={"task_id": task_id})
203
+ output.emit_object(payload, json_out=st.json_out)
204
+
205
+
206
+ @app.command("watch-task")
207
+ def watch_task(
208
+ task_id: str = typer.Argument(...),
209
+ wait: bool = typer.Option(False, "--wait", help="阻塞到终态;失败时非零退出"),
210
+ ) -> None:
211
+ """跟任务进度流(Ctrl-C 退出)。"""
212
+ st = current()
213
+ final: dict | None = None
214
+ for frame in st.client().stream("memory.WatchTask", path_params={"task_id": task_id}):
215
+ final = frame
216
+ status = frame.get("status") or "?"
217
+ if st.json_out:
218
+ output.emit_json(frame)
219
+ else:
220
+ typer.echo(status)
221
+ if wait and status.endswith(("SUCCEEDED", "FAILED", "CANCELED")):
222
+ break
223
+ if wait and final and (final.get("status") or "").endswith(("FAILED", "CANCELED")):
224
+ raise UsageError(final.get("error") or "任务失败")
225
+
226
+
227
+ # ------------------------------------------------------------------ 内部
228
+
229
+ def _checked(memory_path: str | None, allow_empty: bool = False) -> str | None:
230
+ """首段校验:服务端只认 memories/resources,早点报错比往返一趟强。"""
231
+ if not memory_path:
232
+ if allow_empty:
233
+ return None
234
+ raise UsageError(f"需要记忆路径(首段须是 {'/'.join(ROOTS)})")
235
+ head = memory_path.strip("/").split("/", 1)[0]
236
+ if head not in ROOTS:
237
+ raise UsageError(
238
+ f"路径首段 {head!r} 不在域内(只允许 {'、'.join(ROOTS)})"
239
+ f"—— 试 {ROOTS[0]}/{memory_path.lstrip('/')}"
240
+ )
241
+ return memory_path
242
+
243
+
244
+ def _read(file: str | None) -> bytes:
245
+ if not file or file == "-":
246
+ return sys.stdin.buffer.read()
247
+ try:
248
+ with open(file, "rb") as fh:
249
+ return fh.read()
250
+ except OSError as exc:
251
+ raise UsageError(f"读取失败:{exc}") from None
252
+
253
+
254
+ def _bytes(value) -> bytes:
255
+ """protojson 把 bytes 编成 base64;解失败就当纯文本用。"""
256
+ if isinstance(value, bytes):
257
+ return value
258
+ if not value:
259
+ return b""
260
+ try:
261
+ return base64.b64decode(value, validate=True)
262
+ except Exception:
263
+ return str(value).encode()
264
+
265
+
266
+ def _print_tree(nodes, indent: int = 0) -> None:
267
+ for node in nodes or []:
268
+ name = node.get("memoryPath") or "?"
269
+ size = node.get("sizeBytes")
270
+ mark = "/ " if node.get("isDir") else " "
271
+ extra = f" ({size}B)" if size else ""
272
+ typer.echo(" " * indent + mark + str(name) + extra)
273
+ children = node.get("children")
274
+ if children:
275
+ _print_tree(children, indent + 1)
@@ -0,0 +1,70 @@
1
+ """org:组织(团队空间)成员身份与邀请码。
2
+
3
+ 注册时创建的 org 就是 web 的「个人空间」(`is_primary`),其余为团队空间。
4
+ 邀请码一次性:接受即作废,明文只在签发那一次出现。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import typer
10
+
11
+ from .. import output
12
+ from ..state import current
13
+
14
+ app = typer.Typer(no_args_is_help=True, help="组织成员身份 / 邀请码")
15
+
16
+ _COLUMNS = [
17
+ ("ORG ID", "orgId"),
18
+ ("名称", "name"),
19
+ ("slug", "slug"),
20
+ ("角色", "role"),
21
+ ("个人空间", "isPrimary"),
22
+ ]
23
+
24
+
25
+ @app.command("list")
26
+ def list_() -> None:
27
+ """我加入的 org。"""
28
+ st = current()
29
+ payload = st.client().call("tenant.ListMyOrgs")
30
+ output.emit_items(payload, items_key="orgs", columns=_COLUMNS, json_out=st.json_out)
31
+
32
+
33
+ @app.command("invite")
34
+ def invite(
35
+ org_id: str = typer.Argument(...),
36
+ ttl: int = typer.Option(None, "--ttl", help="有效期秒数;<=0 用服务端默认(7 天)"),
37
+ role: str = typer.Option(None, "--role", help="加入后的角色:MEMBER(默认)或 ADMIN"),
38
+ ) -> None:
39
+ """签发成员邀请码(**明文只打印这一次**;OWNER 不发)。"""
40
+ st = current()
41
+ body: dict = {}
42
+ if ttl is not None:
43
+ body["ttlSeconds"] = ttl
44
+ if role:
45
+ body["role"] = role
46
+ payload = st.client().call(
47
+ "tenant.CreateOrgInvitation", path_params={"org_id": org_id}, body=body
48
+ )
49
+ if st.json_out:
50
+ output.emit_json(payload)
51
+ return
52
+ typer.echo(payload.get("code") or "", err=True)
53
+ expires = payload.get("expiresAt")
54
+ typer.echo(
55
+ f"org={payload.get('orgId') or org_id}"
56
+ + (f" 到期 {expires}" if expires else "")
57
+ + "(明文只此一次)",
58
+ err=True,
59
+ )
60
+
61
+
62
+ @app.command("accept")
63
+ def accept(code: str = typer.Argument(..., help="邀请码(一次性,接受即作废)")) -> None:
64
+ """接受邀请加入 org(org 由码反查,不用手填)。"""
65
+ st = current()
66
+ payload = st.client().call("tenant.AcceptOrgInvitation", body={"code": code})
67
+ if st.json_out:
68
+ output.emit_json(payload)
69
+ return
70
+ typer.echo(f"已加入 {payload.get('orgId') or '?'} {payload.get('name') or ''}")