arkclaw-webchat-cli 0.6.0__tar.gz → 0.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/PKG-INFO +1 -1
  2. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/pyproject.toml +1 -1
  3. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/cli.py +25 -7
  4. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/flows.py +174 -64
  5. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/transport/openclaw.py +32 -3
  6. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/.gitignore +0 -0
  7. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/README.md +0 -0
  8. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/__init__.py +0 -0
  9. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/attachments.py +0 -0
  10. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/config.py +0 -0
  11. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/control.py +0 -0
  12. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/core.py +0 -0
  13. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/doctor.py +0 -0
  14. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/errors.py +0 -0
  15. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/identity.py +0 -0
  16. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/oauth.py +0 -0
  17. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/output.py +0 -0
  18. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/policy.py +0 -0
  19. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/providers.py +0 -0
  20. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/secrets_store.py +0 -0
  21. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/sts.py +0 -0
  22. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/transport/__init__.py +0 -0
  23. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/transport/a2a.py +0 -0
  24. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/transport/base.py +0 -0
  25. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/update.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arkclaw-webchat-cli
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: CLI to chat with an ArkClaw EE space's Claw over enterprise SSO — zero permanent AK/SK.
5
5
  Author: ArkClaw Team
6
6
  Keywords: arkclaw,cli,ee,openclaw,sso,sts
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arkclaw-webchat-cli"
7
- version = "0.6.0"
7
+ version = "0.6.1"
8
8
  description = "CLI to chat with an ArkClaw EE space's Claw over enterprise SSO — zero permanent AK/SK."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -161,11 +161,20 @@ def chat(
161
161
  message_opt: str | None = typer.Option(
162
162
  None, "-m", "--message", help="One-shot message (same as the positional argument)."
163
163
  ),
164
+ agent: str = typer.Option(
165
+ "main",
166
+ "--agent",
167
+ help="Which agent in the claw to talk to (its agentId; see `arkclaw agents`). "
168
+ "Defaults to the claw's main agent.",
169
+ ),
164
170
  session: str | None = typer.Option(
165
171
  None,
166
172
  "--session",
167
- help="Named server-side session (conversation thread). Defaults to the "
168
- "claw's main session; sessions survive CLI restarts.",
173
+ help="Resume a specific conversation by its 会话ID (see `arkclaw sessions`). "
174
+ "Omit to continue the agent's main session.",
175
+ ),
176
+ new: bool = typer.Option(
177
+ False, "--new", help="Start a fresh conversation instead of continuing the last one."
169
178
  ),
170
179
  approve_all: bool = typer.Option(
171
180
  False,
@@ -192,6 +201,8 @@ def chat(
192
201
  target=target,
193
202
  message=message_opt or message,
194
203
  session=session,
204
+ agent=agent,
205
+ new=new,
195
206
  approve_all=approve_all,
196
207
  files=list(file) or None,
197
208
  output=output,
@@ -200,10 +211,17 @@ def chat(
200
211
 
201
212
 
202
213
  @app.command()
203
- def sessions(json_mode: bool = _json_opt()) -> None:
204
- """List chat sessions this machine has used (most recent first)."""
214
+ def sessions(
215
+ claw: str | None = typer.Argument(
216
+ None, metavar="[CLAW]", help="Claw id (ci-...). Omit to use your default claw."
217
+ ),
218
+ agent: str = typer.Option("main", "--agent", help="Which agent's conversations to list."),
219
+ json_mode: bool = _json_opt(),
220
+ ) -> None:
221
+ """List an agent's conversations from the server (newest first). Resume one
222
+ with `arkclaw chat <claw> --session <会话ID>`, or start fresh with `--new`."""
205
223
  emitter = Emitter(json_mode=json_mode)
206
- _run(emitter, lambda: flows.do_sessions(emitter))
224
+ _run(emitter, lambda: flows.do_sessions(emitter, clawid=claw, agent=agent))
207
225
 
208
226
 
209
227
  @app.command(name="ls")
@@ -243,8 +261,8 @@ def push(
243
261
 
244
262
  @app.command()
245
263
  def agents(json_mode: bool = _json_opt()) -> None:
246
- """List the agents/claws you can chat with (openclaw: your claws, space
247
- auto-discovered; a2a: the agent at the endpoint). Then `chat <id-or-name>`."""
264
+ """List the agents you can chat with (openclaw: the agents in your accessible
265
+ claws; a2a: the agent at the endpoint). Then `chat <claw-or-name>`."""
248
266
  emitter = Emitter(json_mode=json_mode)
249
267
  _run(emitter, lambda: flows.do_agents(emitter))
250
268
 
@@ -20,6 +20,7 @@ import signal
20
20
  import sys
21
21
  import time
22
22
  import urllib.parse
23
+ import uuid
23
24
  from typing import Any
24
25
 
25
26
  from ee_claw import config as config_mod
@@ -53,7 +54,7 @@ from ee_claw.identity import (
53
54
  from ee_claw.oauth import OAuthClient
54
55
  from ee_claw.output import Emitter
55
56
  from ee_claw.providers import ProviderContext, resolve_token_source
56
- from ee_claw.sts import assume_role_with_oidc, get_chat_token
57
+ from ee_claw.sts import assume_role_with_oidc
57
58
  from ee_claw.transport.a2a import A2ATransport, agent_card
58
59
  from ee_claw.transport.base import Approver, Attachment, Transport, TurnEvent, TurnResult
59
60
  from ee_claw.transport.openclaw import OpenClawTransport, session_key_for
@@ -116,35 +117,77 @@ def _user_identity(id_token: str | None) -> dict[str, str]:
116
117
  return ident
117
118
 
118
119
 
119
- def _probe_claw_access(
120
- cfg: SessionConfig, id_token: str, claw_ids: list[str]
121
- ) -> tuple[set[str], set[str]]:
122
- """Which of ``claw_ids`` the current user can ACTUALLY chat — checked live by
123
- minting a (throwaway) ChatToken for each, the only per-user check the CLI's
124
- role can do. Returns (accessible, denied). A transient/non-permission error
125
- keeps the claw (don't hide it on a network blip). Raises ArkclawError if
126
- creds can't be obtained at all (caller falls back to the unchecked list)."""
120
+ def _agent_display_name(agent: dict[str, Any]) -> str:
121
+ """Friendly name for an agent. The main agent often comes back named "main";
122
+ show a friendly label instead of leaking the internal id (mirrors the web)."""
123
+ name = str(agent.get("name") or "").strip()
124
+ agent_id = str(agent.get("agentId") or "")
125
+ is_main = bool(agent.get("default")) or agent_id == "main"
126
+ if is_main and (not name or name == "main"):
127
+ return "ArkClaw 智能助手"
128
+ return name or agent_id or "(未命名)"
129
+
130
+
131
+ def _list_agents_for_claws(
132
+ cfg: SessionConfig, creds: dict[str, str], identity: dict[str, str], claw_ids: list[str]
133
+ ) -> dict[str, tuple[str, list[dict[str, Any]]]]:
134
+ """For each claw, list its agents over the ws (``arkclaw.team.agent.list``) —
135
+ which also proves access (the ChatToken mint denies non-owners). Returns
136
+ ``clawid → (status, agents)``, status ∈ {"ok","denied","error"}. Parallel."""
127
137
  from concurrent.futures import ThreadPoolExecutor
128
138
 
129
- creds = _openclaw_creds(cfg, id_token) # may raise (offline / expired / no creds)
130
- identity = _user_identity(id_token)
131
139
  region = str(cfg.region)
132
140
 
133
- def check(cid: str) -> tuple[str, bool]:
141
+ def one(clawid: str) -> tuple[str, str, list[dict[str, Any]]]:
142
+ t = OpenClawTransport(clawid=clawid, region=region, creds=creds, identity=identity)
134
143
  try:
135
- get_chat_token(cid, region, creds, identity=identity)
136
- return cid, True
144
+ return clawid, "ok", asyncio.run(t.list_agents())
137
145
  except ClawAccessDeniedError:
138
- return cid, False
146
+ return clawid, "denied", []
139
147
  except ArkclawError:
140
- return cid, True # transient/other — keep, don't silently drop
148
+ return clawid, "error", [] # transient — keep the claw, mark unknown
141
149
 
142
- accessible: set[str] = set()
143
- denied: set[str] = set()
150
+ out: dict[str, tuple[str, list[dict[str, Any]]]] = {}
144
151
  with ThreadPoolExecutor(max_workers=min(8, max(1, len(claw_ids)))) as ex:
145
- for cid, ok in ex.map(check, claw_ids):
146
- (accessible if ok else denied).add(cid)
147
- return accessible, denied
152
+ for clawid, status, agents in ex.map(one, claw_ids):
153
+ out[clawid] = (status, agents)
154
+ return out
155
+
156
+
157
+ def _resolve_session_key(
158
+ cfg: SessionConfig, id_token: str, clawid: str, agent: str, session: str | None, new: bool
159
+ ) -> str | None:
160
+ """Compute the OpenClaw sessionKey ``agent:<agentId>:<label>`` for a chat:
161
+
162
+ * ``--new`` → a fresh ``agent:<agent>:cli-<uuid>`` (new conversation)
163
+ * ``--session <id>`` → resume: a full ``agent:…`` key passes through; a short
164
+ sessionId is resolved to its key via ``sessions.list``
165
+ * neither, agent=main → ``None`` (the default ``agent:main:main`` session)
166
+ * neither, other agent→ ``agent:<agent>:main``
167
+ """
168
+ agent = agent or "main"
169
+ if new:
170
+ return f"agent:{agent}:cli-{uuid.uuid4()}"
171
+ if not session:
172
+ return None if agent == "main" else f"agent:{agent}:main"
173
+ if session.startswith("agent:"):
174
+ return session # already a full sessionKey
175
+ # A hex token is a 会话ID copied from `arkclaw sessions` → resolve to its key.
176
+ if re.fullmatch(r"[0-9a-f]{6,}", session):
177
+ creds = _openclaw_creds(cfg, id_token)
178
+ t = OpenClawTransport(
179
+ clawid=clawid, region=str(cfg.region), creds=creds, identity=_user_identity(id_token)
180
+ )
181
+ for s in asyncio.run(t.list_sessions(agent)):
182
+ sid = str(s.get("sessionId") or "")
183
+ if sid and (sid == session or sid.startswith(session)):
184
+ return str(s.get("key") or f"agent:{agent}:{session}")
185
+ raise ValidationError(
186
+ f"会话 {session!r} 不存在。",
187
+ hint="用 `arkclaw sessions` 查看现有会话,或加 `--new` 开新会话。",
188
+ )
189
+ # Otherwise a human-named session label (created/resumed by this key).
190
+ return f"agent:{agent}:{session}"
148
191
 
149
192
 
150
193
  def _normalize(url: str) -> tuple[str, str]:
@@ -783,6 +826,8 @@ def do_chat(
783
826
  target: str | None = None,
784
827
  message: str | None = None,
785
828
  session: str | None = None,
829
+ agent: str = "main",
830
+ new: bool = False,
786
831
  approve_all: bool = False,
787
832
  files: list[str] | None = None,
788
833
  output: str | None = None,
@@ -838,6 +883,12 @@ def do_chat(
838
883
  )
839
884
 
840
885
  approver = _build_approver(emitter, approve_all=approve_all, interactive=interactive)
886
+ # Resolve agent + session → a concrete sessionKey (openclaw only; a2a uses
887
+ # the session name as the context label).
888
+ if cfg.transport == "openclaw":
889
+ effective_claw = clawid or cfg.claw
890
+ if effective_claw:
891
+ session = _resolve_session_key(cfg, id_token, effective_claw, agent, session, new)
841
892
  target, transport = _target_transport(
842
893
  cfg, clawid, id_token, session=session, approver=approver
843
894
  )
@@ -879,8 +930,9 @@ def do_chat(
879
930
  pass
880
931
  emitter.line(
881
932
  f"ArkClaw chat · {target}"
933
+ + (f" · agent {agent}" if agent and agent != "main" else "")
934
+ + (f" · 会话 {session.split(':')[-1][:16]}" if session else "")
882
935
  + (f" · 空间 {cfg.space}" if cfg.space and cfg.space != target else "")
883
- + (f" · 会话 {session}" if session else "")
884
936
  + " (Ctrl+C 退出)"
885
937
  )
886
938
  while True:
@@ -911,11 +963,54 @@ def do_chat(
911
963
  emitter.line("")
912
964
 
913
965
 
914
- def do_sessions(emitter: Emitter) -> dict[str, Any]:
915
- """``arkclaw sessions``: the local view of server-side sessions (which
916
- space/claw/session tuples this machine has talked to). Server-side
917
- listing + replay land with gate G2."""
918
- rows = config_mod.list_sessions()
966
+ def do_sessions(
967
+ emitter: Emitter, clawid: str | None = None, agent: str = "main"
968
+ ) -> dict[str, Any]:
969
+ """``arkclaw sessions [<claw>] [--agent <id>]``: the conversations of an agent
970
+ in a claw, from the server (ws ``sessions.list``) — newest first. Resume one
971
+ with ``arkclaw chat <claw> --session <会话ID>``. Falls back to the local
972
+ record for a2a / when the claw can't be reached."""
973
+ cfg = config_mod.load_config()
974
+ target = (clawid or (cfg.claw if cfg else None)) if cfg else None
975
+ if cfg and cfg.space and cfg.transport == "openclaw" and target:
976
+ try:
977
+ _, id_token = _load_session(cfg)
978
+ creds = _openclaw_creds(cfg, id_token)
979
+ t = OpenClawTransport(
980
+ clawid=target, region=str(cfg.region), creds=creds, identity=_user_identity(id_token)
981
+ )
982
+ sessions = asyncio.run(t.list_sessions(agent))
983
+ sessions.sort(key=lambda s: float(s.get("updatedAt") or 0), reverse=True)
984
+ emitter.line(f"★ claw {target} · agent {agent} · 空间 {cfg.space}")
985
+ if sessions:
986
+ emitter.table(
987
+ ["标题", "上次活动", "会话ID", "状态"],
988
+ [
989
+ [
990
+ (str(s.get("derivedTitle") or "(无标题)"))[:48],
991
+ _ago_ms(s.get("updatedAt")),
992
+ str(s.get("sessionId") or "")[:8],
993
+ str(s.get("status") or "—"),
994
+ ]
995
+ for s in sessions
996
+ ],
997
+ )
998
+ emitter.line(" 续聊: arkclaw chat <claw> --session <会话ID>;新开: --new")
999
+ else:
1000
+ emitter.line(" 该 agent 还没有会话。`arkclaw chat` 开始第一段对话。")
1001
+ return {
1002
+ "claw": target, "agent": agent,
1003
+ "sessions": [
1004
+ {"sessionId": s.get("sessionId"), "key": s.get("key"),
1005
+ "title": s.get("derivedTitle"), "updatedAt": s.get("updatedAt"),
1006
+ "status": s.get("status")}
1007
+ for s in sessions
1008
+ ],
1009
+ }
1010
+ except ArkclawError as e:
1011
+ emitter.line(f" ⚠ 无法从服务端列会话({e.code}),回退本地记录:")
1012
+
1013
+ rows = config_mod.list_sessions() # a2a / not-logged-in / no-claw / server-error fallback
919
1014
  if rows:
920
1015
  emitter.table(
921
1016
  ["会话", "目标", "空间", "最近使用"],
@@ -964,13 +1059,15 @@ def _ago(ts: object) -> str:
964
1059
 
965
1060
 
966
1061
  def do_agents(emitter: Emitter) -> dict[str, Any]:
967
- """``arkclaw agents``: the agents/claws you can chat with.
1062
+ """``arkclaw agents``: the agents you can chat with.
968
1063
 
969
- openclaw → the claws YOU have used from this machine (kept locally, newest
970
- first), plus your default claw. This is deliberately NOT a server-side
971
- enumeration of the space that API returns every member's claws to anyone
972
- (no per-user scoping is reachable by the CLI's role), so we don't call it.
973
- You add a claw to this list by chatting it once: ``arkclaw chat ci-...``.
1064
+ openclaw → the AGENTS hosted in your accessible claws. The candidate claws
1065
+ are the ones YOU have used from this machine (kept locally, newest first)
1066
+ plus your default; for each we list its agents over the ws
1067
+ (``arkclaw.team.agent.list``), which also verifies access — inaccessible
1068
+ claws are pruned. This is NOT a space-wide server enumeration (not per-user
1069
+ scoped for the CLI's role). You add a claw by chatting it once: ``arkclaw
1070
+ chat ci-...``.
974
1071
 
975
1072
  a2a → the agent at the endpoint (its card). Profiles live under ``arkclaw
976
1073
  profile``, full conversation history under ``arkclaw sessions``."""
@@ -1030,11 +1127,11 @@ def do_agents(emitter: Emitter) -> dict[str, Any]:
1030
1127
  )
1031
1128
  elif cfg.transport == "openclaw":
1032
1129
  emitter.line(f"★ 当前登录(openclaw)· 空间 {cfg.space}")
1033
- # Candidates = the claws YOU have used from this machine (local history) +
1034
- # your default NOT a server enumeration (the space-wide list isn't
1035
- # per-user scoped for the CLI's role). We then VERIFY access to each by
1036
- # minting a throwaway ChatToken, so only claws you can actually chat are
1037
- # shown; denied ones are pruned from history (and cleared as default).
1130
+ # The chat-able units are AGENTS, and one claw hosts several. Candidates
1131
+ # are the claws YOU have used from this machine (local history) + your
1132
+ # default; for each we list its agents over the ws (which also proves
1133
+ # access). Inaccessible claws are pruned from history (+ cleared as
1134
+ # default). NOT a space-wide server enumeration (not per-user scoped).
1038
1135
  seen: dict[str, float] = {}
1039
1136
  for s in config_mod.list_sessions():
1040
1137
  cid = s.get("claw")
@@ -1045,43 +1142,56 @@ def do_agents(emitter: Emitter) -> dict[str, Any]:
1045
1142
  seen[str(cfg.claw)] = 0.0 # the default claw, even if not chatted yet
1046
1143
 
1047
1144
  claw_ids = sorted(seen, key=lambda c: seen[c], reverse=True)
1145
+ rows: list[list[str]] = []
1146
+ agents_data: list[dict[str, Any]] = []
1048
1147
  checked = False
1049
1148
  if claw_ids:
1050
1149
  if not emitter.json:
1051
- emitter.line(" 核对访问权限中…")
1150
+ emitter.line(" 列出 agents…")
1052
1151
  try:
1053
1152
  _, id_token = _load_session(cfg)
1054
- accessible, denied = _probe_claw_access(cfg, id_token, claw_ids)
1153
+ creds = _openclaw_creds(cfg, id_token)
1154
+ identity = _user_identity(id_token)
1155
+ results = _list_agents_for_claws(cfg, creds, identity, claw_ids)
1055
1156
  checked = True
1056
- for cid in denied:
1057
- config_mod.forget_claw(str(cfg.space), cid) # stop listing it
1058
- if cfg.claw == cid:
1059
- config_mod.save_config(dataclasses.replace(cfg, claw=None))
1060
- cfg = config_mod.load_config() or cfg
1061
- current["default_claw"] = cfg.claw
1062
- claw_ids = [c for c in claw_ids if c in accessible]
1157
+ for clawid in claw_ids:
1158
+ status, agents = results[clawid]
1159
+ if status == "denied":
1160
+ config_mod.forget_claw(str(cfg.space), clawid) # stop listing it
1161
+ if cfg.claw == clawid:
1162
+ config_mod.save_config(dataclasses.replace(cfg, claw=None))
1163
+ cfg = config_mod.load_config() or cfg
1164
+ current["default_claw"] = cfg.claw
1165
+ continue
1166
+ if status == "error" or not agents:
1167
+ rows.append(["(无法列出 agent)", "—", clawid, "★" if clawid == cfg.claw else ""])
1168
+ continue
1169
+ for a in agents:
1170
+ aid = str(a.get("agentId") or "")
1171
+ is_main = bool(a.get("default")) or aid == "main"
1172
+ rows.append([
1173
+ _agent_display_name(a), aid or "—", clawid,
1174
+ "★" if (clawid == cfg.claw and is_main) else "",
1175
+ ])
1176
+ agents_data.append({
1177
+ "clawId": clawid, "agentId": aid, "name": _agent_display_name(a),
1178
+ "description": a.get("description"), "default": is_main,
1179
+ })
1063
1180
  except ArkclawError:
1064
- checked = False # offline / expired / no creds → show unchecked
1181
+ checked = False # offline / expired / no creds → can't list agents
1065
1182
 
1066
- claws = [{"ClawInstanceId": cid, "last_used": seen[cid]} for cid in claw_ids]
1067
- current["claws"] = claws
1183
+ current["agents"] = agents_data
1068
1184
  current["source"] = "verified" if checked else "local-history-unchecked"
1069
- if claws:
1185
+ if checked and rows:
1186
+ emitter.table(["Agent", "agentId", "所属 Claw", "默认"], rows)
1187
+ elif checked:
1188
+ emitter.line(" 本空间没有你可访问的 claw / agent(历史里的都已无权或不存在)。")
1189
+ elif claw_ids:
1190
+ emitter.line(" ⚠ 未能连接核对(离线/会话过期),无法列出 agent;以下为本地历史的 claw:")
1070
1191
  emitter.table(
1071
- ["Claw(可 chat)", "上次使用", "默认"],
1072
- [
1073
- [
1074
- str(c["ClawInstanceId"]),
1075
- _ago(c["last_used"]) if c["last_used"] else "—",
1076
- "★" if c["ClawInstanceId"] == cfg.claw else "",
1077
- ]
1078
- for c in claws
1079
- ],
1192
+ ["Claw", "上次使用"],
1193
+ [[c, _ago(seen[c]) if seen[c] else "—"] for c in claw_ids],
1080
1194
  )
1081
- if not checked:
1082
- emitter.line(" ⚠ 未能核对权限(离线/会话过期),以上为本地历史,可能含你无权访问的。")
1083
- elif checked:
1084
- emitter.line(" 你在本空间没有可访问的 claw(历史里的都已无权或不存在)。")
1085
1195
  else:
1086
1196
  emitter.line(" 还没用过任何 claw。用 `arkclaw chat ci-xxxx` 开始,之后这里会记住它。")
1087
1197
 
@@ -451,9 +451,10 @@ class OpenClawTransport:
451
451
  # agents.files.get {agentId, name} → {file:{name,path,size,content}}
452
452
  # agents.files.set {agentId, name, content} → write
453
453
 
454
- async def _request(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
455
- """connect (hello-ok) → one control request → its res payload. Errors
456
- are redacted; the token-bearing URL is never echoed."""
454
+ async def _request(self, method: str, params: dict[str, Any]) -> Any:
455
+ """connect (hello-ok) → one control request → its res payload (dict or
456
+ list, depending on the method). Errors are redacted; the token-bearing
457
+ URL is never echoed."""
457
458
  chat_token, endpoint = get_chat_token(self.clawid, self.region, self.creds, identity=self.identity)
458
459
  url = (
459
460
  f"wss://{endpoint}/?chatToken={urllib.parse.quote(chat_token)}"
@@ -510,3 +511,31 @@ class OpenClawTransport:
510
511
  return await self._request(
511
512
  "agents.files.set", {"agentId": agent_id, "name": name, "content": content}
512
513
  )
514
+
515
+ async def list_agents(self) -> list[dict[str, Any]]:
516
+ """The agents living in this claw (ws RPC ``arkclaw.team.agent.list``).
517
+ Each item: ``{agentId, name, description?, default?}``. Raises
518
+ ClawAccessDeniedError (via the ChatToken mint) if the user can't access
519
+ this claw."""
520
+ payload = await self._request("arkclaw.team.agent.list", {})
521
+ if isinstance(payload, list):
522
+ return [a for a in payload if isinstance(a, dict)]
523
+ if isinstance(payload, dict): # some gateways wrap the array
524
+ for k in ("data", "agents", "list", "items"):
525
+ v = payload.get(k)
526
+ if isinstance(v, list):
527
+ return [a for a in v if isinstance(a, dict)]
528
+ return []
529
+
530
+ async def list_sessions(self, agent_id: str = "main") -> list[dict[str, Any]]:
531
+ """An agent's conversations (ws RPC ``sessions.list``). Each item:
532
+ ``{key, sessionId, derivedTitle?, updatedAt, status, ...}`` — ``key`` is
533
+ the sessionKey to resume with."""
534
+ payload = await self._request(
535
+ "sessions.list", {"agentId": agent_id, "includeDerivedTitles": True}
536
+ )
537
+ if isinstance(payload, dict):
538
+ sessions = payload.get("sessions")
539
+ if isinstance(sessions, list):
540
+ return [s for s in sessions if isinstance(s, dict)]
541
+ return []