arkclaw-webchat-cli 0.2.0__tar.gz → 0.4.0__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.2.0 → arkclaw_webchat_cli-0.4.0}/PKG-INFO +30 -17
  2. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/README.md +29 -16
  3. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/pyproject.toml +1 -1
  4. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/cli.py +14 -0
  5. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/config.py +39 -0
  6. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/flows.py +113 -82
  7. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/.gitignore +0 -0
  8. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/__init__.py +0 -0
  9. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/attachments.py +0 -0
  10. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/control.py +0 -0
  11. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/core.py +0 -0
  12. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/doctor.py +0 -0
  13. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/errors.py +0 -0
  14. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/identity.py +0 -0
  15. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/oauth.py +0 -0
  16. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/output.py +0 -0
  17. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/policy.py +0 -0
  18. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/providers.py +0 -0
  19. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/secrets_store.py +0 -0
  20. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/sts.py +0 -0
  21. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/transport/__init__.py +0 -0
  22. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/transport/a2a.py +0 -0
  23. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/transport/base.py +0 -0
  24. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/src/ee_claw/transport/openclaw.py +0 -0
  25. {arkclaw_webchat_cli-0.2.0 → arkclaw_webchat_cli-0.4.0}/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.2.0
3
+ Version: 0.4.0
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
@@ -29,9 +29,10 @@ own SSO identity, no passwords, no permanent keys.** Works the same for a human
29
29
  at a prompt and for another agent/script (`--json` + exit codes).
30
30
 
31
31
  ```bash
32
- arkclaw login https://your-space.arkclaw-enterprise-bj.volceapi.com # browser SSO, once
33
- arkclaw agents # list the claws you can chat
34
- arkclaw chat ci-xxxxxxxx # talk to one
32
+ arkclaw init # one-time interactive setup (asks only what it can't auto-detect)
33
+ arkclaw login # browser SSO
34
+ arkclaw agents # list the claws you can chat
35
+ arkclaw chat ci-xxxxxxxx
35
36
  ```
36
37
 
37
38
  ---
@@ -39,7 +40,7 @@ arkclaw chat ci-xxxxxxxx # talk to
39
40
  ## What it can do
40
41
 
41
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.
42
- - **List your agents** — `arkclaw agents` shows the claws you can chat with.
43
+ - **List your agents** — `arkclaw agents` shows the claws you've chatted with (kept locally; add one with `arkclaw chat ci-...`).
43
44
  - **Chat** — interactive REPL or one-shot `-m`, streaming token-by-token, with a live "what's it doing" spinner and tool-event trace.
44
45
  - **Talk to a specific claw** — by id (`chat ci-...`) or by name (`chat 答疑助手`).
45
46
  - **Manage a claw's files** — read/write its managed workspace files (`AGENTS.md`, `SOUL.md`, `MEMORY.md`, …) with `ls` / `pull` / `push`.
@@ -68,10 +69,15 @@ uv venv && uv pip install -e .
68
69
  ## Quick start
69
70
 
70
71
  ```bash
71
- # 1. log in to your space (a browser opens; sign in with SSO)
72
- arkclaw login https://your-space.arkclaw-enterprise-bj.volceapi.com
72
+ # 0. one-time setup interactive. Asks for your space address and (only if the
73
+ # space doesn't already publish them) the CLI client + STS role. Saved to
74
+ # ~/.arkclaw/defaults.json so you never type them again.
75
+ arkclaw init
76
+
77
+ # 1. log in (a browser opens; sign in with SSO)
78
+ arkclaw login
73
79
 
74
- # 2. see which claws you can talk to
80
+ # 2. list the claws you've used (empty at first — you add them by chatting)
75
81
  arkclaw agents
76
82
 
77
83
  # 3. chat — interactive…
@@ -80,9 +86,10 @@ arkclaw chat ci-xxxxxxxx
80
86
  arkclaw chat ci-xxxxxxxx -m "用一句话介绍你自己"
81
87
  ```
82
88
 
83
- `login` figures out the rest from the address (your identity pool, region). If
84
- your space hasn't published auto-discovery yet, you may need to point it at the
85
- STS role oncesee **[Configuration](#configuration)**.
89
+ `arkclaw init` resolves everything it can from the address (identity pool,
90
+ region) and only prompts for what's missing. Once your space publishes
91
+ auto-discovery, `init` asks for nothing but the address and you can even skip
92
+ straight to `arkclaw login https://your-space...`. See **[Configuration](#configuration)**.
86
93
 
87
94
  ---
88
95
 
@@ -90,8 +97,9 @@ STS role once — see **[Configuration](#configuration)**.
90
97
 
91
98
  | Command | What it does |
92
99
  |---|---|
93
- | `arkclaw login <space-url>` | Browser SSO login. `--transport a2a --endpoint <url>` for an agent endpoint. `--clawid ci-...` sets a default claw (otherwise the last one you opened in the browser). Bare `arkclaw login` re-logs into the previous space. |
94
- | `arkclaw agents` | List the claws/agents you can chat with. |
100
+ | `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
+ | `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 (local history, newest first) + your default claw. Add a claw by chatting it once (`arkclaw chat ci-...`). Not a space-wide directory. |
95
103
  | `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. |
96
104
  | `arkclaw <name>` | Shortcut: `arkclaw 答疑助手` ≡ `arkclaw chat 答疑助手`. |
97
105
  | `arkclaw ls` | List the claw's managed workspace files. |
@@ -161,9 +169,14 @@ arkclaw ls --clawid ci-other # a different claw
161
169
  ## Configuration
162
170
 
163
171
  `login` resolves config in this order: **explicit flag → `ARKCLAW_*` env →
164
- the space's published discovery → derived from the address**. You normally only
165
- type the address. Until your platform publishes auto-discovery, a deployment may
166
- supply these (non-secret) via env:
172
+ the space's published discovery → `arkclaw init` defaults derived from the
173
+ address**. You normally only type the address.
174
+
175
+ **The easy way — `arkclaw init`** asks once (interactively) for whatever the
176
+ space doesn't yet auto-publish, and saves it. After that, just `arkclaw login`.
177
+
178
+ **The env way** (scripts/CI, or instead of `init`) — supply the same non-secret
179
+ values via env:
167
180
 
168
181
  | Env | Purpose |
169
182
  |---|---|
@@ -171,7 +184,7 @@ supply these (non-secret) via env:
171
184
  | `ARKCLAW_ROLE_TRN` | STS role that mints the ChatToken (openclaw). |
172
185
  | `ARKCLAW_REGION` / `ARKCLAW_SPACE_ID` | Override region / space (usually auto). |
173
186
 
174
- Other config: `~/.arkclaw/session.json` (routing, 0600), `~/.arkclaw/cmdpolicy.json` (tool-approval allow/deny), OS keychain (tokens).
187
+ Other config: `~/.arkclaw/defaults.json` (`init` answers, 0600), `~/.arkclaw/session.json` (routing, 0600), `~/.arkclaw/cmdpolicy.json` (tool-approval allow/deny), OS keychain (tokens).
175
188
 
176
189
  > **Escape hatch (admin/test):** `arkclaw login <url> --transport openclaw --static-creds` uses `VOLCENGINE_ACCESS_KEY`/`SECRET_KEY` from the env instead of SSO — convenient for CI, but it's account-level keys, not identity-through.
177
190
 
@@ -5,9 +5,10 @@ own SSO identity, no passwords, no permanent keys.** Works the same for a human
5
5
  at a prompt and for another agent/script (`--json` + exit codes).
6
6
 
7
7
  ```bash
8
- arkclaw login https://your-space.arkclaw-enterprise-bj.volceapi.com # browser SSO, once
9
- arkclaw agents # list the claws you can chat
10
- arkclaw chat ci-xxxxxxxx # talk to one
8
+ arkclaw init # one-time interactive setup (asks only what it can't auto-detect)
9
+ arkclaw login # browser SSO
10
+ arkclaw agents # list the claws you can chat
11
+ arkclaw chat ci-xxxxxxxx
11
12
  ```
12
13
 
13
14
  ---
@@ -15,7 +16,7 @@ arkclaw chat ci-xxxxxxxx # talk to
15
16
  ## What it can do
16
17
 
17
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.
18
- - **List your agents** — `arkclaw agents` shows the claws you can chat with.
19
+ - **List your agents** — `arkclaw agents` shows the claws you've chatted with (kept locally; add one with `arkclaw chat ci-...`).
19
20
  - **Chat** — interactive REPL or one-shot `-m`, streaming token-by-token, with a live "what's it doing" spinner and tool-event trace.
20
21
  - **Talk to a specific claw** — by id (`chat ci-...`) or by name (`chat 答疑助手`).
21
22
  - **Manage a claw's files** — read/write its managed workspace files (`AGENTS.md`, `SOUL.md`, `MEMORY.md`, …) with `ls` / `pull` / `push`.
@@ -44,10 +45,15 @@ uv venv && uv pip install -e .
44
45
  ## Quick start
45
46
 
46
47
  ```bash
47
- # 1. log in to your space (a browser opens; sign in with SSO)
48
- arkclaw login https://your-space.arkclaw-enterprise-bj.volceapi.com
48
+ # 0. one-time setup interactive. Asks for your space address and (only if the
49
+ # space doesn't already publish them) the CLI client + STS role. Saved to
50
+ # ~/.arkclaw/defaults.json so you never type them again.
51
+ arkclaw init
52
+
53
+ # 1. log in (a browser opens; sign in with SSO)
54
+ arkclaw login
49
55
 
50
- # 2. see which claws you can talk to
56
+ # 2. list the claws you've used (empty at first — you add them by chatting)
51
57
  arkclaw agents
52
58
 
53
59
  # 3. chat — interactive…
@@ -56,9 +62,10 @@ arkclaw chat ci-xxxxxxxx
56
62
  arkclaw chat ci-xxxxxxxx -m "用一句话介绍你自己"
57
63
  ```
58
64
 
59
- `login` figures out the rest from the address (your identity pool, region). If
60
- your space hasn't published auto-discovery yet, you may need to point it at the
61
- STS role oncesee **[Configuration](#configuration)**.
65
+ `arkclaw init` resolves everything it can from the address (identity pool,
66
+ region) and only prompts for what's missing. Once your space publishes
67
+ auto-discovery, `init` asks for nothing but the address and you can even skip
68
+ straight to `arkclaw login https://your-space...`. See **[Configuration](#configuration)**.
62
69
 
63
70
  ---
64
71
 
@@ -66,8 +73,9 @@ STS role once — see **[Configuration](#configuration)**.
66
73
 
67
74
  | Command | What it does |
68
75
  |---|---|
69
- | `arkclaw login <space-url>` | Browser SSO login. `--transport a2a --endpoint <url>` for an agent endpoint. `--clawid ci-...` sets a default claw (otherwise the last one you opened in the browser). Bare `arkclaw login` re-logs into the previous space. |
70
- | `arkclaw agents` | List the claws/agents you can chat with. |
76
+ | `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
+ | `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 (local history, newest first) + your default claw. Add a claw by chatting it once (`arkclaw chat ci-...`). Not a space-wide directory. |
71
79
  | `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. |
72
80
  | `arkclaw <name>` | Shortcut: `arkclaw 答疑助手` ≡ `arkclaw chat 答疑助手`. |
73
81
  | `arkclaw ls` | List the claw's managed workspace files. |
@@ -137,9 +145,14 @@ arkclaw ls --clawid ci-other # a different claw
137
145
  ## Configuration
138
146
 
139
147
  `login` resolves config in this order: **explicit flag → `ARKCLAW_*` env →
140
- the space's published discovery → derived from the address**. You normally only
141
- type the address. Until your platform publishes auto-discovery, a deployment may
142
- supply these (non-secret) via env:
148
+ the space's published discovery → `arkclaw init` defaults derived from the
149
+ address**. You normally only type the address.
150
+
151
+ **The easy way — `arkclaw init`** asks once (interactively) for whatever the
152
+ space doesn't yet auto-publish, and saves it. After that, just `arkclaw login`.
153
+
154
+ **The env way** (scripts/CI, or instead of `init`) — supply the same non-secret
155
+ values via env:
143
156
 
144
157
  | Env | Purpose |
145
158
  |---|---|
@@ -147,7 +160,7 @@ supply these (non-secret) via env:
147
160
  | `ARKCLAW_ROLE_TRN` | STS role that mints the ChatToken (openclaw). |
148
161
  | `ARKCLAW_REGION` / `ARKCLAW_SPACE_ID` | Override region / space (usually auto). |
149
162
 
150
- Other config: `~/.arkclaw/session.json` (routing, 0600), `~/.arkclaw/cmdpolicy.json` (tool-approval allow/deny), OS keychain (tokens).
163
+ Other config: `~/.arkclaw/defaults.json` (`init` answers, 0600), `~/.arkclaw/session.json` (routing, 0600), `~/.arkclaw/cmdpolicy.json` (tool-approval allow/deny), OS keychain (tokens).
151
164
 
152
165
  > **Escape hatch (admin/test):** `arkclaw login <url> --transport openclaw --static-creds` uses `VOLCENGINE_ACCESS_KEY`/`SECRET_KEY` from the env instead of SSO — convenient for CI, but it's account-level keys, not identity-through.
153
166
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arkclaw-webchat-cli"
7
- version = "0.2.0"
7
+ version = "0.4.0"
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"
@@ -47,6 +47,20 @@ def _run(emitter: Emitter, fn: Callable[[], Any]) -> None:
47
47
  raise typer.Exit(emitter.ok(data))
48
48
 
49
49
 
50
+ @app.command()
51
+ def init(
52
+ address: str | None = typer.Argument(
53
+ None, metavar="[ADDRESS]", help="Space login URL; omit to be prompted."
54
+ ),
55
+ json_mode: bool = _json_opt(),
56
+ ) -> None:
57
+ """One-time interactive setup: capture the login address + (only what isn't
58
+ auto-discovered) the CLI client and STS role, so later `arkclaw login`
59
+ needs no env vars or flags. Run this once, then `arkclaw login`."""
60
+ emitter = Emitter(json_mode=json_mode)
61
+ _run(emitter, lambda: flows.do_init(emitter, address))
62
+
63
+
50
64
  @app.command()
51
65
  def login(
52
66
  address: str | None = typer.Argument(
@@ -176,6 +176,45 @@ def list_agent_names() -> list[str]:
176
176
  return sorted(_load_agents())
177
177
 
178
178
 
179
+ # --- init defaults ------------------------------------------------------------
180
+ # What `arkclaw init` captures once (interactively) so later `arkclaw login`
181
+ # needs no env/flags: the address + the non-secret bits a platform-published
182
+ # /.well-known/arkclaw-cli would otherwise supply. Explicit, user-set — NOT the
183
+ # auto-learned cache. A live well-known still overrides these at login time.
184
+
185
+ _DEFAULTS_FILE = "defaults.json"
186
+ _DEFAULT_KEYS = (
187
+ "address", "issuer", "client_id", "transport", "region",
188
+ "role_trn", "provider_trn", "endpoint", "space_id", "clawid",
189
+ )
190
+
191
+
192
+ def _defaults_path() -> pathlib.Path:
193
+ return pathlib.Path.home() / ".arkclaw" / _DEFAULTS_FILE
194
+
195
+
196
+ def load_defaults() -> dict[str, object]:
197
+ """`arkclaw init` config (empty dict if none). Non-secret only."""
198
+ path = _defaults_path()
199
+ if not path.exists():
200
+ return {}
201
+ try:
202
+ data = json.loads(path.read_text())
203
+ except ValueError:
204
+ return {}
205
+ return {k: v for k, v in data.items() if k in _DEFAULT_KEYS and v} if isinstance(data, dict) else {}
206
+
207
+
208
+ def save_defaults(values: dict[str, object]) -> None:
209
+ """Persist `arkclaw init` answers (whitelisted, non-None, non-secret)."""
210
+ keep = {k: v for k, v in values.items() if k in _DEFAULT_KEYS and v}
211
+ _dir()
212
+ fd = os.open(_defaults_path(), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
213
+ with os.fdopen(fd, "w") as f:
214
+ f.write(json.dumps(keep, ensure_ascii=False))
215
+ os.chmod(_defaults_path(), 0o600)
216
+
217
+
179
218
  # --- Profiles (multi-pool / multi-claw) ---------------------------------------
180
219
  # A profile is a named snapshot of the non-secret session config. Tokens are
181
220
  # keyed by space host in the secret store, so switching profiles reattaches to
@@ -11,7 +11,6 @@ and every error is a typed :class:`~ee_claw.errors.ArkclawError`.
11
11
  from __future__ import annotations
12
12
 
13
13
  import asyncio
14
- import dataclasses
15
14
  import json
16
15
  import os
17
16
  import pathlib
@@ -105,6 +104,63 @@ def _is_userpool_address(host: str) -> bool:
105
104
  return ".userpool.auth.id." in host or host.startswith("userpool-")
106
105
 
107
106
 
107
+ def do_init(emitter: Emitter, address: str | None = None) -> dict[str, Any]:
108
+ """``arkclaw init``: one-time interactive setup. Captures the login address
109
+ plus the bits not yet auto-discoverable (the CLI's OAuth client, the STS
110
+ role) so that afterwards ``arkclaw login`` needs no env vars or flags.
111
+ Anything the space already self-describes (issuer, region, and — once the
112
+ platform publishes it — client_id/role) is auto-filled and not asked."""
113
+ if emitter.json or not sys.stdin.isatty():
114
+ raise ValidationError(
115
+ "arkclaw init 是交互式的,需要终端。",
116
+ hint="在终端直接运行 `arkclaw init`;脚本里改用 env(ARKCLAW_CLIENT_ID/ROLE_TRN)+ flags。",
117
+ )
118
+
119
+ def ask(label: str, default: str | None = None, *, required: bool = False) -> str | None:
120
+ suffix = f" [{default}]" if default else ""
121
+ while True:
122
+ ans = input(f" {label}{suffix}: ").strip() or (default or "")
123
+ if ans or not required:
124
+ return ans or None
125
+ emitter.line(" (必填)")
126
+
127
+ emitter.line("arkclaw init —— 一次性配置(能自动发现的不会问你)\n")
128
+ address = address or ask("空间登录地址 (https://…)", required=True)
129
+ base, host = _normalize(str(address))
130
+ disc = discover_cli_config(base) or {}
131
+ issuer = disc.get("issuer") or (f"https://{host}" if _is_userpool_address(host) else None)
132
+ region = disc.get("region") or derive_region(base)
133
+ if issuer:
134
+ emitter.line(" ✓ 自动发现身份池 issuer")
135
+ if region:
136
+ emitter.line(f" ✓ 区域: {region}")
137
+
138
+ # User-facing name is "webchat" (the ChatToken/wss path); internally it's
139
+ # the "openclaw" transport. "a2a" is unchanged.
140
+ disc_t = str(disc.get("transport") or "")
141
+ transport_in = ask("传输方式 (webchat/a2a)", default=("a2a" if disc_t == "a2a" else "webchat")) or "webchat"
142
+ transport = "a2a" if transport_in.lower() == "a2a" else "openclaw"
143
+ client_id = disc.get("client_id") or ask(
144
+ "CLI client_id(public OAuth 客户端,问管理员;平台发布发现后免此项)", required=True
145
+ )
146
+ role_trn: object = None
147
+ endpoint: object = None
148
+ if transport == "openclaw":
149
+ role_trn = disc.get("role_trn") or ask("STS role TRN (trn:iam::<账号>:role/<名字>)", required=True)
150
+ else:
151
+ endpoint = disc.get("endpoint") or ask("A2A endpoint (https://…)", required=True)
152
+ clawid = disc.get("clawid") or ask("默认 claw id (ci-…,可留空,登录后用 arkclaw agents 选)")
153
+
154
+ saved = {
155
+ "address": base, "issuer": issuer, "client_id": client_id, "transport": transport,
156
+ "region": region, "role_trn": role_trn, "provider_trn": disc.get("provider_trn"),
157
+ "endpoint": endpoint, "space_id": disc.get("space_id"), "clawid": clawid,
158
+ }
159
+ config_mod.save_defaults(saved)
160
+ emitter.line("\n✓ 配置已存到 ~/.arkclaw/defaults.json。现在运行: arkclaw login")
161
+ return {k: v for k, v in saved.items() if v}
162
+
163
+
108
164
  def do_login(
109
165
  url: str | None,
110
166
  emitter: Emitter,
@@ -138,31 +194,34 @@ def do_login(
138
194
  A bare ``arkclaw login`` (no URL) re-logs into the previous space,
139
195
  inheriting every non-secret setting from the last login.
140
196
  """
197
+ defaults = config_mod.load_defaults() # what `arkclaw init` captured, if any
141
198
  if not url:
142
199
  prev = config_mod.load_config()
143
- if not prev or not (prev.url or prev.space):
200
+ if prev and (prev.url or prev.space):
201
+ url = prev.url or f"https://{prev.space}"
202
+ emitter.line(f"↻ 重新登录上次的空间: {url}({prev.transport})")
203
+ transport = transport or prev.transport
204
+ endpoint = endpoint or prev.endpoint
205
+ region = region or prev.region
206
+ role_trn = role_trn or prev.role_trn
207
+ provider_trn = provider_trn or prev.provider_trn
208
+ issuer = issuer or prev.issuer
209
+ client_id = client_id or prev.client_id
210
+ clawid = clawid or prev.claw
211
+ elif defaults.get("address"):
212
+ url = str(defaults["address"])
213
+ emitter.line(f"↻ 用 arkclaw init 的配置登录: {url}")
214
+ else:
144
215
  raise ValidationError(
145
216
  "缺少空间地址(首次登录必填)。",
146
- hint="例: arkclaw login https://<space-url> [--transport a2a];"
147
- "之后裸 `arkclaw login` 即可续登上次的空间。",
217
+ hint="先跑 `arkclaw init` 配置一次,或 `arkclaw login https://<space-url>`。",
148
218
  )
149
- url = prev.url or f"https://{prev.space}"
150
- emitter.line(f"↻ 重新登录上次的空间: {url}({prev.transport})")
151
- transport = transport or prev.transport
152
- endpoint = endpoint or prev.endpoint
153
- region = region or prev.region
154
- role_trn = role_trn or prev.role_trn
155
- provider_trn = provider_trn or prev.provider_trn
156
- issuer = issuer or prev.issuer
157
- client_id = client_id or prev.client_id
158
- clawid = clawid or prev.claw
159
219
  base, host = _normalize(url)
160
220
 
161
- # The space self-describes via GET /.well-known/arkclaw-cli (non-secret:
162
- # issuer/client_id/transport/region/space_id/role_trn/provider_trn/clawid).
163
- # When the platform publishes it, the user gives ONLY the address — every
164
- # field below resolves from it. Order: explicit flag > env > well-known.
165
- disc = discover_cli_config(base) or {}
221
+ # Resolution: explicit flag > env > the space's published
222
+ # /.well-known/arkclaw-cli > `arkclaw init` defaults > derived. The live
223
+ # well-known overrides init defaults; nothing is hardcoded per space.
224
+ disc = {**defaults, **(discover_cli_config(base) or {})}
166
225
  transport = transport or str(disc.get("transport") or "") or "openclaw"
167
226
  clawid = clawid or disc.get("clawid") # type: ignore[assignment]
168
227
  region = (
@@ -810,11 +869,16 @@ def _ago(ts: object) -> str:
810
869
 
811
870
 
812
871
  def do_agents(emitter: Emitter) -> dict[str, Any]:
813
- """``arkclaw agents``: the agents/claws you can chat with — nothing else.
814
- openclaw → the claws across your space(s) (space auto-discovered if login
815
- didn't pin one); a2a the agent at the endpoint (its card). Each is
816
- recorded so ``chat <name>`` works. Profiles live under ``arkclaw profile``,
817
- recent sessions under ``arkclaw sessions``. Human view is tabular."""
872
+ """``arkclaw agents``: the agents/claws you can chat with.
873
+
874
+ openclaw the claws YOU have used from this machine (kept locally, newest
875
+ first), plus your default claw. This is deliberately NOT a server-side
876
+ enumeration of the space that API returns every member's claws to anyone
877
+ (no per-user scoping is reachable by the CLI's role), so we don't call it.
878
+ You add a claw to this list by chatting it once: ``arkclaw chat ci-...``.
879
+
880
+ a2a → the agent at the endpoint (its card). Profiles live under ``arkclaw
881
+ profile``, full conversation history under ``arkclaw sessions``."""
818
882
  cfg = config_mod.load_config()
819
883
  if not cfg or not cfg.space:
820
884
  raise NoLoginError("尚未登录。", hint="先运行: arkclaw login <空间地址>")
@@ -871,73 +935,40 @@ def do_agents(emitter: Emitter) -> dict[str, Any]:
871
935
  )
872
936
  elif cfg.transport == "openclaw":
873
937
  emitter.line(f"★ 当前登录(openclaw)· 空间 {cfg.space}")
874
- _, id_token = _load_session()
875
- try:
876
- creds: dict[str, str] | None = _openclaw_creds(cfg, id_token)
877
- except ArkclawError as e:
878
- creds = None
879
- current["claws_error"] = {"code": e.code, "message": e.message}
880
- claws: list[dict[str, Any]] = []
881
- if creds is not None:
882
- # Which space(s) to list? If login didn't pin a space_id, discover
883
- # the user's spaces so `agents` lists the chat-able claws from the
884
- # address alone. Listing may be denied by a least-privilege role.
885
- space_ids: list[str] = [str(cfg.space_id)] if cfg.space_id else []
886
- if not space_ids:
887
- try:
888
- spaces = control.call_action(
889
- "ListClawSpaces", {"ProjectName": "default", "MaxResults": 50},
890
- region=str(cfg.region), creds=creds,
891
- )
892
- assert isinstance(spaces, dict)
893
- space_ids = [
894
- str(s["SpaceId"]) for s in (spaces.get("Spaces") or [])
895
- if isinstance(s, dict) and s.get("SpaceId")
896
- ]
897
- if len(space_ids) == 1: # cache it so later calls skip discovery
898
- config_mod.save_config(dataclasses.replace(cfg, space_id=space_ids[0]))
899
- except ArkclawError as e:
900
- current["claws_error"] = {"code": e.code, "message": e.message}
901
- for sid in space_ids:
902
- try:
903
- result = redact_obj(control.call_action(
904
- "ListClawInstances",
905
- {"SpaceId": sid, "ProjectName": "default", "MaxResults": 100},
906
- region=str(cfg.region), creds=creds,
907
- ))
908
- assert isinstance(result, dict)
909
- items = result.get("Items") or result.get("ClawInstances") or []
910
- claws += [it for it in items if isinstance(it, dict)]
911
- except ArkclawError as e:
912
- current["claws_error"] = {"code": e.code, "message": e.message}
938
+ # The claws you can chat = the ones YOU have used from this machine,
939
+ # remembered locally — NOT a server enumeration of the space. The
940
+ # space-wide ListClawInstances returns every member's claws to anyone
941
+ # (no per-user scoping reachable by the CLI's role), so we don't call it.
942
+ # New claws enter via `arkclaw chat ci-...`; each chat is recorded
943
+ # (record_session) and shows up here next time.
944
+ seen: dict[str, float] = {}
945
+ for s in config_mod.list_sessions():
946
+ cid = s.get("claw")
947
+ if s.get("space") == cfg.space and cid:
948
+ ts = s.get("last_used")
949
+ seen[str(cid)] = max(seen.get(str(cid), 0.0), float(ts) if isinstance(ts, (int, float)) else 0.0)
950
+ if cfg.claw and str(cfg.claw) not in seen:
951
+ seen[str(cfg.claw)] = 0.0 # the default claw, even if not chatted yet
952
+ claws = [
953
+ {"ClawInstanceId": cid, "last_used": ts}
954
+ for cid, ts in sorted(seen.items(), key=lambda kv: kv[1], reverse=True)
955
+ ]
913
956
  current["claws"] = claws
914
- for it in claws: # claw Name → chat <名字>
915
- cid = it.get("ClawInstanceId") or it.get("Id")
916
- if it.get("Name") and cid:
917
- config_mod.record_agent(str(it["Name"]), dataclasses.replace(cfg, claw=str(cid)))
957
+ current["source"] = "local-history"
918
958
  if claws:
919
959
  emitter.table(
920
- ["Claw(可 chat)", "名称", "状态", "默认"],
960
+ ["Claw(可 chat)", "上次使用", "默认"],
921
961
  [
922
962
  [
923
- it.get("ClawInstanceId") or it.get("Id") or "—",
924
- it.get("Name", ""),
925
- it.get("Status", ""),
926
- "★" if (it.get("ClawInstanceId") or it.get("Id")) == cfg.claw else "",
963
+ str(c["ClawInstanceId"]),
964
+ _ago(c["last_used"]) if c["last_used"] else "—",
965
+ "" if c["ClawInstanceId"] == cfg.claw else "",
927
966
  ]
928
- for it in claws
967
+ for c in claws
929
968
  ],
930
969
  )
931
970
  else:
932
- err = current.get("claws_error")
933
- emitter.table(
934
- ["Claw(可 chat)", "状态", "说明"],
935
- [[
936
- cfg.claw or "(用 --clawid 直连)",
937
- "默认目标" if cfg.claw else "—",
938
- f"列表不可用: {err['code']}" if isinstance(err, dict) else "没有可用 claw",
939
- ]],
940
- )
971
+ emitter.line(" 还没用过任何 claw。用 `arkclaw chat ci-xxxx` 开始,之后这里会记住它。")
941
972
 
942
973
  # `agents` is the list of agents you can chat — nothing else. Saved
943
974
  # profiles live under `arkclaw profile`, recent sessions under