arkclaw-webchat-cli 0.6.0__tar.gz → 0.6.2__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.
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/PKG-INFO +8 -5
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/README.md +7 -4
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/pyproject.toml +1 -1
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/cli.py +91 -9
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/flows.py +280 -73
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/transport/openclaw.py +61 -3
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/.gitignore +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/__init__.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/attachments.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/config.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/control.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/core.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/doctor.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/errors.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/identity.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/oauth.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/output.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/policy.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/providers.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/secrets_store.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/sts.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/transport/__init__.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/transport/a2a.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/transport/base.py +0 -0
- {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/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.
|
|
3
|
+
Version: 0.6.2
|
|
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
|
|
@@ -31,7 +31,7 @@ at a prompt and for another agent/script (`--json` + exit codes).
|
|
|
31
31
|
```bash
|
|
32
32
|
arkclaw init # one-time interactive setup (asks only what it can't auto-detect)
|
|
33
33
|
arkclaw login # browser SSO
|
|
34
|
-
arkclaw agents # list the
|
|
34
|
+
arkclaw agents # list the agents you can chat
|
|
35
35
|
arkclaw chat ci-xxxxxxxx
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -40,7 +40,8 @@ arkclaw chat ci-xxxxxxxx
|
|
|
40
40
|
## What it can do
|
|
41
41
|
|
|
42
42
|
- **Log in as you** — browser SSO (PKCE), short-lived token, auto-refresh. No AK/SK, no client secret, nothing permanent on disk but a token in your OS keychain.
|
|
43
|
-
- **List your agents** — `arkclaw agents` shows the
|
|
43
|
+
- **List your agents** — `arkclaw agents` shows the agents in your accessible claws (add a claw by chatting it once: `arkclaw chat ci-...`).
|
|
44
|
+
- **Manage agents** — `agents create` / `agents delete` add or remove named agents in a claw, right from the terminal.
|
|
44
45
|
- **Chat** — interactive REPL or one-shot `-m`, streaming token-by-token, with a live "what's it doing" spinner and tool-event trace.
|
|
45
46
|
- **Talk to a specific claw** — by id (`chat ci-...`) or by name (`chat 答疑助手`).
|
|
46
47
|
- **Manage a claw's files** — read/write its managed workspace files (`AGENTS.md`, `SOUL.md`, `MEMORY.md`, …) with `ls` / `pull` / `push`.
|
|
@@ -99,14 +100,16 @@ straight to `arkclaw login https://your-space...`. See **[Configuration](#config
|
|
|
99
100
|
|---|---|
|
|
100
101
|
| `arkclaw init [address]` | One-time interactive setup. Saves the address + (only what isn't auto-discovered) the CLI client and STS role to `~/.arkclaw/defaults.json`, so later commands need no env/flags. |
|
|
101
102
|
| `arkclaw login [space-url]` | Browser SSO login. Uses `init` defaults if you omit the URL. `--transport a2a --endpoint <url>` for an agent endpoint. `--clawid ci-...` sets a default claw. Bare `arkclaw login` re-logs into the previous space. |
|
|
102
|
-
| `arkclaw agents` | List the claws you've used from this machine
|
|
103
|
+
| `arkclaw agents` | List the agents in your accessible claws (verified over the ws; inaccessible claws are pruned). Candidate claws = the ones you've used from this machine + your default — add one by chatting it once (`arkclaw chat ci-...`). Not a space-wide directory. |
|
|
104
|
+
| `arkclaw agents create` | Create a named agent in a claw: `--name` `--role` `--soul` (+ optional `--description`, repeatable `--skill`). Then `chat <claw> --agent <agentId>`. |
|
|
105
|
+
| `arkclaw agents delete <agent>` | Delete a named agent by agentId (`a-...`) or display name. Asks for confirmation unless `--yes`; the `main` agent is protected. |
|
|
103
106
|
| `arkclaw chat [TARGET] [MSG]` | Chat. `TARGET` = a claw id (`ci-...`), an agent name (from `agents`), or a profile. With `MSG` → one-shot; without → interactive REPL. `-f` attach files, `-o` write the reply, `--session NAME` name the conversation, `--approve-all` auto-approve tool runs. |
|
|
104
107
|
| `arkclaw <name>` | Shortcut: `arkclaw 答疑助手` ≡ `arkclaw chat 答疑助手`. |
|
|
105
108
|
| `arkclaw ls` | List the claw's managed workspace files. |
|
|
106
109
|
| `arkclaw pull <name> [local]` | Download a managed file to local disk. |
|
|
107
110
|
| `arkclaw push <local> [name]` | Write a local text file into a managed file. |
|
|
108
111
|
| `arkclaw fanout "<msg>" --clawid ci-a --clawid ci-b` | Same message to many claws in parallel. |
|
|
109
|
-
| `arkclaw sessions` |
|
|
112
|
+
| `arkclaw sessions [claw]` | An agent's conversations from the server (`--agent` to pick one), newest first — resume with `chat --session <会话ID>`, fork with `--new`. Falls back to the local record when offline / a2a. |
|
|
110
113
|
| `arkclaw profile save/use/list` | Named snapshots of the session config (multi-space / multi-claw). |
|
|
111
114
|
| `arkclaw doctor` | Self-check: login freshness, keychain, pool/STS/endpoint reachability. |
|
|
112
115
|
| `arkclaw schema --json` | Machine-readable command surface (for agents/tooling). |
|
|
@@ -7,7 +7,7 @@ at a prompt and for another agent/script (`--json` + exit codes).
|
|
|
7
7
|
```bash
|
|
8
8
|
arkclaw init # one-time interactive setup (asks only what it can't auto-detect)
|
|
9
9
|
arkclaw login # browser SSO
|
|
10
|
-
arkclaw agents # list the
|
|
10
|
+
arkclaw agents # list the agents you can chat
|
|
11
11
|
arkclaw chat ci-xxxxxxxx
|
|
12
12
|
```
|
|
13
13
|
|
|
@@ -16,7 +16,8 @@ arkclaw chat ci-xxxxxxxx
|
|
|
16
16
|
## What it can do
|
|
17
17
|
|
|
18
18
|
- **Log in as you** — browser SSO (PKCE), short-lived token, auto-refresh. No AK/SK, no client secret, nothing permanent on disk but a token in your OS keychain.
|
|
19
|
-
- **List your agents** — `arkclaw agents` shows the
|
|
19
|
+
- **List your agents** — `arkclaw agents` shows the agents in your accessible claws (add a claw by chatting it once: `arkclaw chat ci-...`).
|
|
20
|
+
- **Manage agents** — `agents create` / `agents delete` add or remove named agents in a claw, right from the terminal.
|
|
20
21
|
- **Chat** — interactive REPL or one-shot `-m`, streaming token-by-token, with a live "what's it doing" spinner and tool-event trace.
|
|
21
22
|
- **Talk to a specific claw** — by id (`chat ci-...`) or by name (`chat 答疑助手`).
|
|
22
23
|
- **Manage a claw's files** — read/write its managed workspace files (`AGENTS.md`, `SOUL.md`, `MEMORY.md`, …) with `ls` / `pull` / `push`.
|
|
@@ -75,14 +76,16 @@ straight to `arkclaw login https://your-space...`. See **[Configuration](#config
|
|
|
75
76
|
|---|---|
|
|
76
77
|
| `arkclaw init [address]` | One-time interactive setup. Saves the address + (only what isn't auto-discovered) the CLI client and STS role to `~/.arkclaw/defaults.json`, so later commands need no env/flags. |
|
|
77
78
|
| `arkclaw login [space-url]` | Browser SSO login. Uses `init` defaults if you omit the URL. `--transport a2a --endpoint <url>` for an agent endpoint. `--clawid ci-...` sets a default claw. Bare `arkclaw login` re-logs into the previous space. |
|
|
78
|
-
| `arkclaw agents` | List the claws you've used from this machine
|
|
79
|
+
| `arkclaw agents` | List the agents in your accessible claws (verified over the ws; inaccessible claws are pruned). Candidate claws = the ones you've used from this machine + your default — add one by chatting it once (`arkclaw chat ci-...`). Not a space-wide directory. |
|
|
80
|
+
| `arkclaw agents create` | Create a named agent in a claw: `--name` `--role` `--soul` (+ optional `--description`, repeatable `--skill`). Then `chat <claw> --agent <agentId>`. |
|
|
81
|
+
| `arkclaw agents delete <agent>` | Delete a named agent by agentId (`a-...`) or display name. Asks for confirmation unless `--yes`; the `main` agent is protected. |
|
|
79
82
|
| `arkclaw chat [TARGET] [MSG]` | Chat. `TARGET` = a claw id (`ci-...`), an agent name (from `agents`), or a profile. With `MSG` → one-shot; without → interactive REPL. `-f` attach files, `-o` write the reply, `--session NAME` name the conversation, `--approve-all` auto-approve tool runs. |
|
|
80
83
|
| `arkclaw <name>` | Shortcut: `arkclaw 答疑助手` ≡ `arkclaw chat 答疑助手`. |
|
|
81
84
|
| `arkclaw ls` | List the claw's managed workspace files. |
|
|
82
85
|
| `arkclaw pull <name> [local]` | Download a managed file to local disk. |
|
|
83
86
|
| `arkclaw push <local> [name]` | Write a local text file into a managed file. |
|
|
84
87
|
| `arkclaw fanout "<msg>" --clawid ci-a --clawid ci-b` | Same message to many claws in parallel. |
|
|
85
|
-
| `arkclaw sessions` |
|
|
88
|
+
| `arkclaw sessions [claw]` | An agent's conversations from the server (`--agent` to pick one), newest first — resume with `chat --session <会话ID>`, fork with `--new`. Falls back to the local record when offline / a2a. |
|
|
86
89
|
| `arkclaw profile save/use/list` | Named snapshots of the session config (multi-space / multi-claw). |
|
|
87
90
|
| `arkclaw doctor` | Self-check: login freshness, keychain, pool/STS/endpoint reachability. |
|
|
88
91
|
| `arkclaw schema --json` | Machine-readable command surface (for agents/tooling). |
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "arkclaw-webchat-cli"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.2"
|
|
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="
|
|
168
|
-
"
|
|
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(
|
|
204
|
-
|
|
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")
|
|
@@ -241,14 +259,76 @@ def push(
|
|
|
241
259
|
_run(emitter, lambda: flows.do_push(local, remote, emitter, clawid=clawid))
|
|
242
260
|
|
|
243
261
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
262
|
+
agents_app = typer.Typer()
|
|
263
|
+
app.add_typer(agents_app, name="agents")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@agents_app.callback(invoke_without_command=True)
|
|
267
|
+
def agents(ctx: typer.Context, json_mode: bool = _json_opt()) -> None:
|
|
268
|
+
"""List the agents you can chat with (openclaw: the agents in your accessible
|
|
269
|
+
claws; a2a: the agent at the endpoint). Then `chat <claw-or-name>`."""
|
|
270
|
+
if ctx.invoked_subcommand is not None:
|
|
271
|
+
# `agents --json create …` must honor the group-level flag — stash it
|
|
272
|
+
# for the subcommand (click contexts inherit obj), never drop it.
|
|
273
|
+
ctx.obj = {"json": json_mode}
|
|
274
|
+
return
|
|
248
275
|
emitter = Emitter(json_mode=json_mode)
|
|
249
276
|
_run(emitter, lambda: flows.do_agents(emitter))
|
|
250
277
|
|
|
251
278
|
|
|
279
|
+
def _group_json(ctx: typer.Context, json_mode: bool) -> bool:
|
|
280
|
+
"""A subcommand's effective --json: its own flag OR the group-level one."""
|
|
281
|
+
return json_mode or bool((ctx.obj or {}).get("json"))
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@agents_app.command("create")
|
|
285
|
+
def agents_create(
|
|
286
|
+
ctx: typer.Context,
|
|
287
|
+
name: str = typer.Option(..., "--name", help="Agent display name."),
|
|
288
|
+
role: str = typer.Option(..., "--role", help="One-line role (what this agent does)."),
|
|
289
|
+
soul: str = typer.Option(
|
|
290
|
+
..., "--soul", help="Persona / system instructions (the agent's SOUL.md)."
|
|
291
|
+
),
|
|
292
|
+
description: str | None = typer.Option(None, "--description", help="Optional description."),
|
|
293
|
+
skill: list[str] = typer.Option(
|
|
294
|
+
[], "--skill", help="Skill to enable (repeat for several)."
|
|
295
|
+
),
|
|
296
|
+
clawid: str | None = typer.Option(
|
|
297
|
+
None, "--clawid", help="Claw instance id (ci-...). Omit to use your default claw."
|
|
298
|
+
),
|
|
299
|
+
json_mode: bool = _json_opt(),
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Create a named agent in a claw. Then `arkclaw chat <claw> --agent <agentId>`."""
|
|
302
|
+
emitter = Emitter(json_mode=_group_json(ctx, json_mode))
|
|
303
|
+
_run(
|
|
304
|
+
emitter,
|
|
305
|
+
lambda: flows.do_agents_create(
|
|
306
|
+
emitter,
|
|
307
|
+
name=name,
|
|
308
|
+
role=role,
|
|
309
|
+
soul=soul,
|
|
310
|
+
description=description,
|
|
311
|
+
skills=list(skill),
|
|
312
|
+
clawid=clawid,
|
|
313
|
+
),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@agents_app.command("delete")
|
|
318
|
+
def agents_delete(
|
|
319
|
+
ctx: typer.Context,
|
|
320
|
+
agent: str = typer.Argument(..., help="agentId (a-...) or display name to delete."),
|
|
321
|
+
clawid: str | None = typer.Option(
|
|
322
|
+
None, "--clawid", help="Claw instance id (ci-...). Omit to use your default claw."
|
|
323
|
+
),
|
|
324
|
+
yes: bool = typer.Option(False, "--yes", help="Skip the confirmation prompt."),
|
|
325
|
+
json_mode: bool = _json_opt(),
|
|
326
|
+
) -> None:
|
|
327
|
+
"""Delete a named agent from a claw (asks for confirmation unless --yes)."""
|
|
328
|
+
emitter = Emitter(json_mode=_group_json(ctx, json_mode))
|
|
329
|
+
_run(emitter, lambda: flows.do_agents_delete(emitter, agent, clawid=clawid, yes=yes))
|
|
330
|
+
|
|
331
|
+
|
|
252
332
|
@app.command()
|
|
253
333
|
def fanout(
|
|
254
334
|
message: str = typer.Argument(..., help="The message to send to every claw."),
|
|
@@ -394,6 +474,8 @@ def _schema_data() -> dict:
|
|
|
394
474
|
for name, cmd in sorted(getattr(root, "commands", {}).items()):
|
|
395
475
|
subs = getattr(cmd, "commands", None)
|
|
396
476
|
if subs:
|
|
477
|
+
if getattr(cmd, "invoke_without_command", False):
|
|
478
|
+
commands.append(info(name, cmd)) # the bare group is a command too
|
|
397
479
|
commands.extend(info(f"{name} {s}", c) for s, c in sorted(subs.items()))
|
|
398
480
|
else:
|
|
399
481
|
commands.append(info(name, cmd))
|
|
@@ -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
|
|
@@ -51,9 +52,9 @@ from ee_claw.identity import (
|
|
|
51
52
|
read_chrome_recent_claw,
|
|
52
53
|
)
|
|
53
54
|
from ee_claw.oauth import OAuthClient
|
|
54
|
-
from ee_claw.output import Emitter
|
|
55
|
+
from ee_claw.output import Emitter, sanitize
|
|
55
56
|
from ee_claw.providers import ProviderContext, resolve_token_source
|
|
56
|
-
from ee_claw.sts import assume_role_with_oidc
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
""
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
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
|
-
|
|
136
|
-
return cid, True
|
|
144
|
+
return clawid, "ok", asyncio.run(t.list_agents())
|
|
137
145
|
except ClawAccessDeniedError:
|
|
138
|
-
return
|
|
146
|
+
return clawid, "denied", []
|
|
139
147
|
except ArkclawError:
|
|
140
|
-
return
|
|
148
|
+
return clawid, "error", [] # transient — keep the claw, mark unknown
|
|
141
149
|
|
|
142
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
return
|
|
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(
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
|
1062
|
+
"""``arkclaw agents``: the agents you can chat with.
|
|
968
1063
|
|
|
969
|
-
openclaw → the
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
(
|
|
973
|
-
|
|
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
|
-
#
|
|
1034
|
-
#
|
|
1035
|
-
#
|
|
1036
|
-
#
|
|
1037
|
-
#
|
|
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
|
-
|
|
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
|
|
1057
|
-
|
|
1058
|
-
if
|
|
1059
|
-
config_mod.
|
|
1060
|
-
cfg
|
|
1061
|
-
|
|
1062
|
-
|
|
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 →
|
|
1181
|
+
checked = False # offline / expired / no creds → can't list agents
|
|
1065
1182
|
|
|
1066
|
-
|
|
1067
|
-
current["claws"] = claws
|
|
1183
|
+
current["agents"] = agents_data
|
|
1068
1184
|
current["source"] = "verified" if checked else "local-history-unchecked"
|
|
1069
|
-
if
|
|
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
|
|
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
|
|
|
@@ -1091,15 +1201,14 @@ def do_agents(emitter: Emitter) -> dict[str, Any]:
|
|
|
1091
1201
|
return {"current": current}
|
|
1092
1202
|
|
|
1093
1203
|
|
|
1094
|
-
def
|
|
1095
|
-
|
|
1096
|
-
|
|
1204
|
+
def _openclaw_claw_transport(
|
|
1205
|
+
clawid: str | None, *, feature: str, hint: str
|
|
1206
|
+
) -> tuple[OpenClawTransport, str]:
|
|
1207
|
+
"""Build an OpenClawTransport against the given (or default) claw for an
|
|
1208
|
+
openclaw-only feature. Returns (transport, clawid)."""
|
|
1097
1209
|
cfg, id_token = _load_session()
|
|
1098
1210
|
if cfg.transport != "openclaw":
|
|
1099
|
-
raise ValidationError(
|
|
1100
|
-
"文件传输目前仅支持 openclaw transport(claw 工作区文件)。",
|
|
1101
|
-
hint="A2A agent 没有可直传的工作区文件系统。",
|
|
1102
|
-
)
|
|
1211
|
+
raise ValidationError(f"{feature}目前仅支持 openclaw transport。", hint=hint)
|
|
1103
1212
|
clawid = clawid or cfg.claw
|
|
1104
1213
|
if not clawid:
|
|
1105
1214
|
raise NoClawError("未指定 claw。", hint="加 --clawid <ci-...>。")
|
|
@@ -1111,6 +1220,104 @@ def _openclaw_file_transport(clawid: str | None) -> tuple[OpenClawTransport, str
|
|
|
1111
1220
|
), clawid
|
|
1112
1221
|
|
|
1113
1222
|
|
|
1223
|
+
def _openclaw_file_transport(clawid: str | None) -> tuple[OpenClawTransport, str]:
|
|
1224
|
+
"""Build an OpenClawTransport for file transfer (openclaw-only). Returns
|
|
1225
|
+
(transport, clawid)."""
|
|
1226
|
+
return _openclaw_claw_transport(
|
|
1227
|
+
clawid, feature="文件传输", hint="A2A agent 没有可直传的工作区文件系统。"
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def _agent_mgmt_transport(clawid: str | None) -> tuple[OpenClawTransport, str]:
|
|
1232
|
+
return _openclaw_claw_transport(
|
|
1233
|
+
clawid, feature="agent 管理", hint="A2A 模式连接的是单个 agent 端点,没有可管理的 claw。"
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def do_agents_create(
|
|
1238
|
+
emitter: Emitter,
|
|
1239
|
+
*,
|
|
1240
|
+
name: str,
|
|
1241
|
+
role: str,
|
|
1242
|
+
soul: str,
|
|
1243
|
+
description: str | None = None,
|
|
1244
|
+
skills: list[str] | None = None,
|
|
1245
|
+
clawid: str | None = None,
|
|
1246
|
+
) -> dict[str, Any]:
|
|
1247
|
+
"""``arkclaw agents create``: create a named agent in a claw (ws RPC
|
|
1248
|
+
``arkclaw.team.agent.create``; live-calibrated 2026-06-10). The server
|
|
1249
|
+
requires name+role+soul; skills may be empty."""
|
|
1250
|
+
for label, value in (("--name", name), ("--role", role), ("--soul", soul)):
|
|
1251
|
+
if not value.strip():
|
|
1252
|
+
raise ValidationError(f"{label} 不能为空。")
|
|
1253
|
+
if name.strip().startswith("a-") or name.strip() == "main":
|
|
1254
|
+
raise ValidationError(
|
|
1255
|
+
f"--name 不能是 {name.strip()!r} — 会与 agentId 命名空间(a-…/main)冲突。"
|
|
1256
|
+
)
|
|
1257
|
+
transport, clawid = _agent_mgmt_transport(clawid)
|
|
1258
|
+
raw = asyncio.run(
|
|
1259
|
+
transport.create_agent(
|
|
1260
|
+
name=name, role=role, soul=soul, description=description, skills=skills or []
|
|
1261
|
+
)
|
|
1262
|
+
)
|
|
1263
|
+
agent = redact_obj(raw)
|
|
1264
|
+
assert isinstance(agent, dict) # redact_obj preserves the dict shape
|
|
1265
|
+
aid = str(agent.get("agentId") or "")
|
|
1266
|
+
emitter.line(f"✓ 已创建 agent {agent.get('name') or name}({aid or '?'})· claw {clawid}")
|
|
1267
|
+
if aid:
|
|
1268
|
+
emitter.line(f" 开聊: arkclaw chat {clawid} --agent {aid}")
|
|
1269
|
+
return {"claw": clawid, "agent": agent}
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
def do_agents_delete(
|
|
1273
|
+
emitter: Emitter, agent: str, *, clawid: str | None = None, yes: bool = False
|
|
1274
|
+
) -> dict[str, Any]:
|
|
1275
|
+
"""``arkclaw agents delete``: remove a named agent from a claw (ws RPC
|
|
1276
|
+
``arkclaw.team.agent.delete``). Accepts an agentId (``a-…``) or the agent's
|
|
1277
|
+
display name; either way the target is resolved against the claw's live
|
|
1278
|
+
agent list first — so a typo'd id can't delete anything, an ``a-…``-looking
|
|
1279
|
+
display name can't shadow another agent's id, and the claw's default agent
|
|
1280
|
+
(``main`` or ``default: true``) stays protected."""
|
|
1281
|
+
if agent == "main": # fail fast — no network needed to refuse this
|
|
1282
|
+
raise ValidationError("默认(main)agent 不能删除。")
|
|
1283
|
+
transport, clawid = _agent_mgmt_transport(clawid)
|
|
1284
|
+
agents = asyncio.run(transport.list_agents())
|
|
1285
|
+
matches = [a for a in agents if str(a.get("agentId") or "") == agent]
|
|
1286
|
+
if not matches: # not an id → try the display name
|
|
1287
|
+
matches = [a for a in agents if str(a.get("name") or "") == agent]
|
|
1288
|
+
if len(matches) > 1:
|
|
1289
|
+
ids = ", ".join(str(a.get("agentId") or "?") for a in matches)
|
|
1290
|
+
raise ValidationError(
|
|
1291
|
+
f"名称 {agent!r} 对应多个 agent({ids}),请直接用 agentId。"
|
|
1292
|
+
)
|
|
1293
|
+
if not matches:
|
|
1294
|
+
raise ValidationError(
|
|
1295
|
+
f"claw {clawid} 里没有 agentId 或名称为 {agent!r} 的 agent。",
|
|
1296
|
+
hint="用 `arkclaw agents` 查看 agentId 与名称。",
|
|
1297
|
+
)
|
|
1298
|
+
target = matches[0]
|
|
1299
|
+
agent_id = str(target.get("agentId") or "")
|
|
1300
|
+
if not agent_id:
|
|
1301
|
+
raise ValidationError(f"服务端没有给出 {agent!r} 的 agentId,无法安全删除。")
|
|
1302
|
+
if agent_id == "main" or bool(target.get("default")):
|
|
1303
|
+
raise ValidationError("默认(main)agent 不能删除。")
|
|
1304
|
+
if not yes:
|
|
1305
|
+
if emitter.json or not sys.stdin.isatty():
|
|
1306
|
+
raise ValidationError("headless 删除必须显式 --yes。")
|
|
1307
|
+
# agent_id is upstream content: sanitize before it touches the TTY —
|
|
1308
|
+
# input() bypasses Emitter.line's control-char stripping.
|
|
1309
|
+
prompt = sanitize(f"确认删除 claw {clawid} 的 agent {agent_id}? [y/N] ")
|
|
1310
|
+
answer = input(prompt).strip().lower()
|
|
1311
|
+
if answer not in ("y", "yes"):
|
|
1312
|
+
emitter.line("已取消。")
|
|
1313
|
+
return {"cancelled": True}
|
|
1314
|
+
result = redact_obj(asyncio.run(transport.delete_agent(agent_id)))
|
|
1315
|
+
if isinstance(result, dict) and result.get("success") is False:
|
|
1316
|
+
raise NetworkError(f"claw 未确认删除 agent {agent_id}(success=false)。")
|
|
1317
|
+
emitter.line(f"✓ 已删除 agent {agent_id} · claw {clawid}")
|
|
1318
|
+
return {"claw": clawid, "agentId": agent_id, "result": result}
|
|
1319
|
+
|
|
1320
|
+
|
|
1114
1321
|
def do_ls(emitter: Emitter, *, clawid: str | None = None) -> dict[str, Any]:
|
|
1115
1322
|
"""``arkclaw ls``: list files in the claw workspace."""
|
|
1116
1323
|
transport, clawid = _openclaw_file_transport(clawid)
|
|
@@ -1316,7 +1523,7 @@ def do_claw(
|
|
|
1316
1523
|
if op == "delete" and not yes:
|
|
1317
1524
|
if emitter.json or not sys.stdin.isatty():
|
|
1318
1525
|
raise ValidationError("headless 删除必须显式 --yes。")
|
|
1319
|
-
answer = input(f"确认删除 claw {clawid}? [y/N] ").strip().lower()
|
|
1526
|
+
answer = input(sanitize(f"确认删除 claw {clawid}? [y/N] ")).strip().lower()
|
|
1320
1527
|
if answer not in ("y", "yes"):
|
|
1321
1528
|
emitter.line("已取消。")
|
|
1322
1529
|
return {"cancelled": True}
|
|
@@ -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]) ->
|
|
455
|
-
"""connect (hello-ok) → one control request → its res payload
|
|
456
|
-
are redacted; the token-bearing
|
|
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,60 @@ 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 create_agent(
|
|
531
|
+
self,
|
|
532
|
+
*,
|
|
533
|
+
name: str,
|
|
534
|
+
role: str,
|
|
535
|
+
soul: str,
|
|
536
|
+
description: str | None = None,
|
|
537
|
+
skills: Sequence[str] = (),
|
|
538
|
+
) -> dict[str, Any]:
|
|
539
|
+
"""Create a named agent in this claw (ws RPC ``arkclaw.team.agent.create``).
|
|
540
|
+
The server requires name + role + soul + skills (array, may be empty);
|
|
541
|
+
returns the created agent ``{agentId, name, role, soul, …}``."""
|
|
542
|
+
params: dict[str, Any] = {
|
|
543
|
+
"name": name,
|
|
544
|
+
"role": role,
|
|
545
|
+
"soul": soul,
|
|
546
|
+
"skills": list(skills),
|
|
547
|
+
}
|
|
548
|
+
if description:
|
|
549
|
+
params["description"] = description
|
|
550
|
+
payload = await self._request("arkclaw.team.agent.create", params)
|
|
551
|
+
return payload if isinstance(payload, dict) else {}
|
|
552
|
+
|
|
553
|
+
async def delete_agent(self, agent_id: str) -> dict[str, Any]:
|
|
554
|
+
"""Delete an agent from this claw (ws RPC ``arkclaw.team.agent.delete``).
|
|
555
|
+
Returns the server ack (``{success: true}``)."""
|
|
556
|
+
payload = await self._request("arkclaw.team.agent.delete", {"agentId": agent_id})
|
|
557
|
+
return payload if isinstance(payload, dict) else {}
|
|
558
|
+
|
|
559
|
+
async def list_sessions(self, agent_id: str = "main") -> list[dict[str, Any]]:
|
|
560
|
+
"""An agent's conversations (ws RPC ``sessions.list``). Each item:
|
|
561
|
+
``{key, sessionId, derivedTitle?, updatedAt, status, ...}`` — ``key`` is
|
|
562
|
+
the sessionKey to resume with."""
|
|
563
|
+
payload = await self._request(
|
|
564
|
+
"sessions.list", {"agentId": agent_id, "includeDerivedTitles": True}
|
|
565
|
+
)
|
|
566
|
+
if isinstance(payload, dict):
|
|
567
|
+
sessions = payload.get("sessions")
|
|
568
|
+
if isinstance(sessions, list):
|
|
569
|
+
return [s for s in sessions if isinstance(s, dict)]
|
|
570
|
+
return []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|