cinna-cli 0.2.0__tar.gz → 0.2.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.
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/PKG-INFO +47 -1
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/README.md +46 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/docs/README.md +18 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/pyproject.toml +1 -1
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/account.py +382 -12
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/context.py +10 -2
- cinna_cli-0.2.2/src/cinna/doctor.py +454 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/main.py +81 -6
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/mcp_proxy.py +87 -11
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/sync_session.py +30 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/tests/test_account.py +298 -8
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/tests/test_context.py +3 -0
- cinna_cli-0.2.2/tests/test_doctor.py +245 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/uv.lock +1 -1
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/.github/workflows/publish.yml +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/.gitignore +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/LICENSE.md +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/docs/interface.md +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/docs/mutagen_capabilities.md +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/__init__.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/auth.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/bootstrap.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/client.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/config.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/console.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/errors.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/logging.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/mutagen_runtime.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/sync.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/sync_ssh_shim.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/sync_tui.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/templates/ACCOUNT_CLAUDE.md.template +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/templates/CLAUDE.md.template +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/src/cinna/templates/__init__.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/tests/__init__.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/tests/conftest.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/tests/test_auth.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/tests/test_bootstrap.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/tests/test_client.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/tests/test_config.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/tests/test_main.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/tests/test_mutagen_runtime.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/tests/test_sync.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/tests/test_sync_session.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.2}/tests/test_sync_ssh_shim.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cinna-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Local development CLI for Cinna Core agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/opencinna/cinna-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/opencinna/cinna-cli
|
|
@@ -112,6 +112,31 @@ cd hr-manager-agent/
|
|
|
112
112
|
cinna set-token yWo36tbkdAOzrALxOEKq31_OA2iMelEg
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
+
### `cinna login [domain]`
|
|
116
|
+
|
|
117
|
+
Sign in to an account workspace in the browser — **no setup token to paste**. One command serves two cases:
|
|
118
|
+
|
|
119
|
+
- **Resume** — run it from inside an existing account workspace and it refreshes the stored account CLI token **in place** (reusing the platform URL + machine name; other settings like the active user workspace are preserved).
|
|
120
|
+
- **Connect new** — run it anywhere else and it bootstraps a fresh account workspace. It asks for the platform **domain** (or pass it as an argument — protocol optional, `localhost` recognized), then creates the workspace **in the current folder if it's empty**, or **in a subfolder you name** if it isn't. So you can `mkdir my-cinna && cd my-cinna && cinna login`, or just `cinna login app.example.com` straight into the current directory.
|
|
121
|
+
|
|
122
|
+
Either way it opens a browser authorization URL (OAuth 2.0 device flow); once you click **Authorize** (already signed in to the platform) the CLI receives a fresh token and writes `.cinna/account.json`.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Resume the account workspace you're in:
|
|
126
|
+
cd my-cinna/ && cinna login
|
|
127
|
+
|
|
128
|
+
# Connect a new account from a fresh folder:
|
|
129
|
+
mkdir my-cinna && cd my-cinna && cinna login # prompts for the domain
|
|
130
|
+
cinna login app.example.com # or pass it directly
|
|
131
|
+
cinna login app.example.com --dir my-cinna # always into a named subfolder
|
|
132
|
+
# Your verification code: WX7K-9Q2P
|
|
133
|
+
# Open this URL and click Authorize:
|
|
134
|
+
# https://app.example.com/device?code=WX7K-9Q2P
|
|
135
|
+
# ✓ Account workspace ready.
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Use it when `cinna account status` or `cinna doctor` reports the account token has expired. Because the per-agent tokens minted from an account (`cinna agent sync`) can only be re-minted while the account token is valid, the flow is: `cinna login` (refresh the account), then `cinna doctor` (re-mint the dependent sub-agent tokens). If the platform doesn't expose the device-login endpoints yet, `cinna login` says so and points you at the `cinna account setup` paste fallback.
|
|
139
|
+
|
|
115
140
|
### `cinna account setup <token_or_url>`
|
|
116
141
|
|
|
117
142
|
Initialize an **account workspace** — a multi-agent root from which you can discover your agents and attach per-agent workspaces without going back to the UI. Setup is initiated from **Settings → Channels → Local Development** on the platform, which emits a `curl | python3` one-liner (same pattern as the per-agent flow):
|
|
@@ -285,6 +310,27 @@ List every agent registered on this machine (from `~/.cinna/agents.json`). Three
|
|
|
285
310
|
2. **Location** — workspace path on top, platform UI link below. Missing directories are flagged in red.
|
|
286
311
|
3. **Sync** — Mutagen session state on top (`active` / `paused` / `connecting` / `error`), plus a per-agent backend probe (`valid token` / `expired token` / `no connection`) on the bottom. The probes run in parallel with a short timeout so the view stays snappy even with many registered agents.
|
|
287
312
|
|
|
313
|
+
### `cinna doctor`
|
|
314
|
+
|
|
315
|
+
Diagnose and repair stale sync state across the whole machine. Over time the per-user registry (`~/.cinna/agents.json`) and the Mutagen daemon drift out of sync as agents are deleted, environments are spun down, and tokens expire — `cinna doctor` reconciles the two and heals the leftovers in one pass. Run it from anywhere; it is not workspace-scoped.
|
|
316
|
+
|
|
317
|
+
It detects and fixes:
|
|
318
|
+
|
|
319
|
+
- **Deleted workspaces** — registry entries whose workspace folder (or its `.cinna/config.json`) is gone. The entry is removed, along with any leftover Mutagen session.
|
|
320
|
+
- **Halted sessions** — sessions stopped on `halted-on-root-deletion` (the local `workspace/` root was deleted) while the agent dir is otherwise intact. Terminated; `cinna dev` recreates a clean one.
|
|
321
|
+
- **Dead-remote sessions** — sessions stuck retrying a remote env that no longer exists (`connecting-beta` / beta polling error). Mutagen has **no** "give up after N failures" option — a session retries forever until paused or terminated — so `doctor` is the cleanup path for these. Terminated.
|
|
322
|
+
- **Orphaned sessions** — `cinna-*` sessions with no registry entry at all. Terminated.
|
|
323
|
+
- **Expired tokens** — for **account-managed** workspaces (those under an account root), the CLI token is re-minted automatically through the parent account token, no pasting required. **Standalone** workspaces (set up via `cinna setup`) can only be refreshed with a pasted setup token, so they are reported with a `cinna set-token` hint rather than changed.
|
|
324
|
+
- **Expired account token** — a sub-agent token can only be re-minted while the **account** token that mints it is still valid. When the account token has itself expired, doctor probes it once and surfaces a single _"renew the account token — run `cinna login`"_ finding (listing the blocked sub-agents) instead of a pile of re-mints that would all fail with 401. Run `cinna login`, then re-run `cinna doctor` to re-mint the dependents.
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
cinna doctor # diagnose, then apply all fixes behind one confirmation
|
|
328
|
+
cinna doctor --dry-run # report problems only; change nothing
|
|
329
|
+
cinna doctor --yes # apply every fix non-interactively
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The diagnosis is split into two tables: **Will fix** (everything doctor can repair — deleted-workspace cleanup, halted/dead/orphaned session termination, account token re-mints) and **No automatic fix — manual action needed** (standalone expired tokens, which need a pasted setup token and are never touched). Everything actionable is applied together behind a single `Apply N fix(es)?` confirmation, so the count always matches the "Will fix" table.
|
|
333
|
+
|
|
288
334
|
### `cinna disconnect`
|
|
289
335
|
|
|
290
336
|
Stop sync, remove `.cinna/` config and generated files (`CLAUDE.md`, `BUILDING_AGENT.md`, `.mcp.json`, `opencode.json`, `mutagen.yml`). Workspace files are preserved.
|
|
@@ -75,6 +75,31 @@ cd hr-manager-agent/
|
|
|
75
75
|
cinna set-token yWo36tbkdAOzrALxOEKq31_OA2iMelEg
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
### `cinna login [domain]`
|
|
79
|
+
|
|
80
|
+
Sign in to an account workspace in the browser — **no setup token to paste**. One command serves two cases:
|
|
81
|
+
|
|
82
|
+
- **Resume** — run it from inside an existing account workspace and it refreshes the stored account CLI token **in place** (reusing the platform URL + machine name; other settings like the active user workspace are preserved).
|
|
83
|
+
- **Connect new** — run it anywhere else and it bootstraps a fresh account workspace. It asks for the platform **domain** (or pass it as an argument — protocol optional, `localhost` recognized), then creates the workspace **in the current folder if it's empty**, or **in a subfolder you name** if it isn't. So you can `mkdir my-cinna && cd my-cinna && cinna login`, or just `cinna login app.example.com` straight into the current directory.
|
|
84
|
+
|
|
85
|
+
Either way it opens a browser authorization URL (OAuth 2.0 device flow); once you click **Authorize** (already signed in to the platform) the CLI receives a fresh token and writes `.cinna/account.json`.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Resume the account workspace you're in:
|
|
89
|
+
cd my-cinna/ && cinna login
|
|
90
|
+
|
|
91
|
+
# Connect a new account from a fresh folder:
|
|
92
|
+
mkdir my-cinna && cd my-cinna && cinna login # prompts for the domain
|
|
93
|
+
cinna login app.example.com # or pass it directly
|
|
94
|
+
cinna login app.example.com --dir my-cinna # always into a named subfolder
|
|
95
|
+
# Your verification code: WX7K-9Q2P
|
|
96
|
+
# Open this URL and click Authorize:
|
|
97
|
+
# https://app.example.com/device?code=WX7K-9Q2P
|
|
98
|
+
# ✓ Account workspace ready.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Use it when `cinna account status` or `cinna doctor` reports the account token has expired. Because the per-agent tokens minted from an account (`cinna agent sync`) can only be re-minted while the account token is valid, the flow is: `cinna login` (refresh the account), then `cinna doctor` (re-mint the dependent sub-agent tokens). If the platform doesn't expose the device-login endpoints yet, `cinna login` says so and points you at the `cinna account setup` paste fallback.
|
|
102
|
+
|
|
78
103
|
### `cinna account setup <token_or_url>`
|
|
79
104
|
|
|
80
105
|
Initialize an **account workspace** — a multi-agent root from which you can discover your agents and attach per-agent workspaces without going back to the UI. Setup is initiated from **Settings → Channels → Local Development** on the platform, which emits a `curl | python3` one-liner (same pattern as the per-agent flow):
|
|
@@ -248,6 +273,27 @@ List every agent registered on this machine (from `~/.cinna/agents.json`). Three
|
|
|
248
273
|
2. **Location** — workspace path on top, platform UI link below. Missing directories are flagged in red.
|
|
249
274
|
3. **Sync** — Mutagen session state on top (`active` / `paused` / `connecting` / `error`), plus a per-agent backend probe (`valid token` / `expired token` / `no connection`) on the bottom. The probes run in parallel with a short timeout so the view stays snappy even with many registered agents.
|
|
250
275
|
|
|
276
|
+
### `cinna doctor`
|
|
277
|
+
|
|
278
|
+
Diagnose and repair stale sync state across the whole machine. Over time the per-user registry (`~/.cinna/agents.json`) and the Mutagen daemon drift out of sync as agents are deleted, environments are spun down, and tokens expire — `cinna doctor` reconciles the two and heals the leftovers in one pass. Run it from anywhere; it is not workspace-scoped.
|
|
279
|
+
|
|
280
|
+
It detects and fixes:
|
|
281
|
+
|
|
282
|
+
- **Deleted workspaces** — registry entries whose workspace folder (or its `.cinna/config.json`) is gone. The entry is removed, along with any leftover Mutagen session.
|
|
283
|
+
- **Halted sessions** — sessions stopped on `halted-on-root-deletion` (the local `workspace/` root was deleted) while the agent dir is otherwise intact. Terminated; `cinna dev` recreates a clean one.
|
|
284
|
+
- **Dead-remote sessions** — sessions stuck retrying a remote env that no longer exists (`connecting-beta` / beta polling error). Mutagen has **no** "give up after N failures" option — a session retries forever until paused or terminated — so `doctor` is the cleanup path for these. Terminated.
|
|
285
|
+
- **Orphaned sessions** — `cinna-*` sessions with no registry entry at all. Terminated.
|
|
286
|
+
- **Expired tokens** — for **account-managed** workspaces (those under an account root), the CLI token is re-minted automatically through the parent account token, no pasting required. **Standalone** workspaces (set up via `cinna setup`) can only be refreshed with a pasted setup token, so they are reported with a `cinna set-token` hint rather than changed.
|
|
287
|
+
- **Expired account token** — a sub-agent token can only be re-minted while the **account** token that mints it is still valid. When the account token has itself expired, doctor probes it once and surfaces a single _"renew the account token — run `cinna login`"_ finding (listing the blocked sub-agents) instead of a pile of re-mints that would all fail with 401. Run `cinna login`, then re-run `cinna doctor` to re-mint the dependents.
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
cinna doctor # diagnose, then apply all fixes behind one confirmation
|
|
291
|
+
cinna doctor --dry-run # report problems only; change nothing
|
|
292
|
+
cinna doctor --yes # apply every fix non-interactively
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The diagnosis is split into two tables: **Will fix** (everything doctor can repair — deleted-workspace cleanup, halted/dead/orphaned session termination, account token re-mints) and **No automatic fix — manual action needed** (standalone expired tokens, which need a pasted setup token and are never touched). Everything actionable is applied together behind a single `Apply N fix(es)?` confirmation, so the count always matches the "Will fix" table.
|
|
296
|
+
|
|
251
297
|
### `cinna disconnect`
|
|
252
298
|
|
|
253
299
|
Stop sync, remove `.cinna/` config and generated files (`CLAUDE.md`, `BUILDING_AGENT.md`, `.mcp.json`, `opencode.json`, `mutagen.yml`). Workspace files are preserved.
|
|
@@ -132,6 +132,13 @@ Key properties:
|
|
|
132
132
|
- **Probeable** — `cinna list` and `cinna status` call `GET /sync-runtime` as a cheap authenticated probe and label the token `valid` / `expired` / `no connection`.
|
|
133
133
|
- **Refreshable in place** — `cinna set-token <token_or_url>` re-exchanges a fresh setup token through `POST /api/cli-setup/{token}` and rewrites both stores without re-cloning the workspace. The refresh is bound to the agent already in the workspace: if the exchanged token belongs to a different agent, the command aborts.
|
|
134
134
|
|
|
135
|
+
### Account CLI Token
|
|
136
|
+
|
|
137
|
+
A second token type (`token_type="cli-account"`) issued to an **account workspace** (`.cinna/account.json`). Scoped only to the `/account/*` routes — it discovers agents and mints per-agent CLI tokens (`cinna agent sync`), but cannot itself sync or exec. Same 7-day rolling expiry as a CLI token.
|
|
138
|
+
|
|
139
|
+
- **Refreshable without a paste** — `cinna login` runs an RFC 8628 device-authorization flow: the CLI prints a short code + URL, the user clicks **Authorize** in the browser (already signed in), and the CLI swaps the fresh token into `.cinna/account.json` in place. Run from an empty/new folder, the same command instead bootstraps a brand-new account workspace.
|
|
140
|
+
- **Mints child tokens** — per-agent tokens minted from it carry its id as provenance and are re-mintable via `POST /account/agents/{id}/mint` (used by `cinna agent sync` and `cinna doctor`).
|
|
141
|
+
|
|
135
142
|
### Knowledge Source
|
|
136
143
|
|
|
137
144
|
A documentation/data source attached to an agent. Queried via the MCP proxy's `knowledge_query` tool, backed by the platform's vector search.
|
|
@@ -190,6 +197,8 @@ An open protocol for connecting AI tools to external data/capabilities. The CLI
|
|
|
190
197
|
main.py (CLI commands — Click)
|
|
191
198
|
│
|
|
192
199
|
├── bootstrap.py — setup orchestration
|
|
200
|
+
├── account.py — account workspace; `cinna login` (device auth), `cinna account`, `cinna agent`
|
|
201
|
+
├── doctor.py — `cinna doctor`: reconcile registry ↔ Mutagen, repair stale state, refresh tokens
|
|
193
202
|
├── config.py — .cinna/config.json: load/save/find
|
|
194
203
|
├── auth.py — JWT storage, Authorization headers
|
|
195
204
|
├── client.py — PlatformClient: HTTP + SSE stream_exec
|
|
@@ -393,6 +402,10 @@ Backend validates on every request:
|
|
|
393
402
|
|
|
394
403
|
When a CLI token expires (or is revoked) the normal remedy is `cinna set-token <token_or_url>` from inside the agent directory. Internally it reuses `_exchange_setup_token()` from `bootstrap.py` — the same helper that backs `cinna setup` — so the server side is a plain re-run of `POST /api/cli-setup/{token}`. The workspace's existing `platform_url` is used as the fallback when a bare token is pasted, which lets agents registered against different platforms each refresh from their own directory without extra flags. The workspace tarball is **not** re-downloaded, and `CLAUDE.md` / `BUILDING_AGENT.md` / `.mcp.json` / `opencode.json` are left in place — only the stored CLI token changes.
|
|
395
404
|
|
|
405
|
+
**Account tokens** refresh without a paste. `cinna login` (run inside an account workspace) drives the platform's RFC 8628 device-authorization flow: `POST /account/login/start` returns a short code + verification URL, the user clicks **Authorize** in the browser (already signed in), and the CLI polls `POST /account/login/poll` until it receives the fresh token, which it writes back into `.cinna/account.json` in place. The poll endpoint always returns HTTP 200 with a `status` field (`authorization_pending` / `slow_down` / `authorized` / `access_denied` / `expired_token`) — a deliberate divergence from RFC 8628's 400+`error` shape. Run from a fresh folder, the same command bootstraps a new account workspace instead of resuming one.
|
|
406
|
+
|
|
407
|
+
**Bulk repair** is `cinna doctor`. It reconciles the `~/.cinna/agents.json` registry against the Mutagen daemon and heals the state that drifts as agents come and go — registry entries whose workspace was deleted, sessions halted on a deleted local root or stuck retrying a dead remote env, and orphaned sessions (Mutagen has no "stop after N failures" knob, so these retry forever until terminated). Expired **per-agent** tokens under an account workspace are re-minted automatically through the parent account token; when the **account** token has itself expired, doctor groups the blocked agents into a single "run `cinna login`" finding instead of attempting re-mints that would 401. Standalone agents are reported for a manual `cinna set-token`.
|
|
408
|
+
|
|
396
409
|
### Authorization
|
|
397
410
|
|
|
398
411
|
| Resource | Rule |
|
|
@@ -423,6 +436,11 @@ When a CLI token expires (or is revoked) the normal remedy is `cinna set-token <
|
|
|
423
436
|
| GET | `/api/v1/cli/agents/{id}/sync-runtime` | CLI JWT | Required Mutagen version + hash (also used by `cinna list` / `cinna status` as a cheap token-validity probe) |
|
|
424
437
|
| POST | `/api/v1/cli/agents/{id}/exec` | CLI JWT | Streaming SSE command execution |
|
|
425
438
|
| WSS | `/api/v1/cli/agents/{id}/sync-stream` | CLI JWT | Mutagen transport tunnel |
|
|
439
|
+
| POST | `/api/v1/cli/account/login/start` | None | Begin a `cinna login` device-authorization request |
|
|
440
|
+
| POST | `/api/v1/cli/account/login/poll` | None | Poll a `cinna login` request — always HTTP 200 + `status` |
|
|
441
|
+
| POST | `/api/v1/cli/account/agents/{id}/mint` | Account token | Mint a per-agent CLI token (`cinna agent sync`, `cinna doctor` re-mint) |
|
|
442
|
+
|
|
443
|
+
The account-workspace surface adds the broader `/api/v1/cli/account/*` route group (login, agents, credentials, connect, schedules, status, api-proxy); only the routes the sync / login / doctor paths use are listed here.
|
|
426
444
|
|
|
427
445
|
Endpoints that were part of the old Docker-replica model (`build-context`, `workspace` POST, `workspace/manifest`, `credentials`) have been removed from the backend and from this CLI.
|
|
428
446
|
|
|
@@ -23,6 +23,8 @@ import os
|
|
|
23
23
|
import platform
|
|
24
24
|
import re
|
|
25
25
|
import sys
|
|
26
|
+
import time
|
|
27
|
+
import webbrowser
|
|
26
28
|
from dataclasses import asdict, dataclass
|
|
27
29
|
from pathlib import Path
|
|
28
30
|
from urllib.parse import urlparse
|
|
@@ -161,6 +163,44 @@ def parse_account_setup_input(raw_input: str) -> tuple[str, str]:
|
|
|
161
163
|
return platform_url, text
|
|
162
164
|
|
|
163
165
|
|
|
166
|
+
def default_account_dir_name(platform_url: str) -> str:
|
|
167
|
+
"""Derive a default workspace folder name from the platform domain.
|
|
168
|
+
|
|
169
|
+
e.g. ``https://demo-core.opencinna.io`` → ``demo-core_opencinna_io``.
|
|
170
|
+
Hostname only (creds/port stripped); every run of non
|
|
171
|
+
``[A-Za-z0-9-]`` characters collapses to a single underscore. Falls back to
|
|
172
|
+
``DEFAULT_ACCOUNT_DIR`` when the URL has no usable host.
|
|
173
|
+
"""
|
|
174
|
+
host = urlparse(platform_url).netloc or platform_url.strip()
|
|
175
|
+
host = host.split("@")[-1].split(":")[0] # strip user:pass@ and :port
|
|
176
|
+
slug = re.sub(r"[^A-Za-z0-9-]+", "_", host).strip("_")
|
|
177
|
+
return slug or DEFAULT_ACCOUNT_DIR
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _prompt_account_dir(default: str) -> str:
|
|
181
|
+
"""Ask for the workspace folder name, offering ``default``.
|
|
182
|
+
|
|
183
|
+
Works in the ``curl ... | python3 -`` bootstrap too: there stdin carries the
|
|
184
|
+
installer script (not keystrokes), so when stdin is not a TTY we talk to the
|
|
185
|
+
controlling terminal via ``/dev/tty`` as long as stdout is a TTY. With no
|
|
186
|
+
terminal attached (CI, captured output) we return ``default`` unchanged so
|
|
187
|
+
non-interactive runs stay non-interactive.
|
|
188
|
+
"""
|
|
189
|
+
if sys.stdin.isatty():
|
|
190
|
+
return click.prompt("Workspace folder name", default=default)
|
|
191
|
+
|
|
192
|
+
if not sys.stdout.isatty():
|
|
193
|
+
return default
|
|
194
|
+
try:
|
|
195
|
+
with open("/dev/tty", "r+") as tty:
|
|
196
|
+
tty.write(f"Workspace folder name [{default}]: ")
|
|
197
|
+
tty.flush()
|
|
198
|
+
line = tty.readline()
|
|
199
|
+
except OSError:
|
|
200
|
+
return default
|
|
201
|
+
return line.strip() or default
|
|
202
|
+
|
|
203
|
+
|
|
164
204
|
def _exchange_account_setup_token(
|
|
165
205
|
platform_url: str, token: str, machine_name: str
|
|
166
206
|
) -> dict:
|
|
@@ -286,6 +326,311 @@ def probe_account_token(config: AccountConfig) -> str:
|
|
|
286
326
|
return "unreachable"
|
|
287
327
|
|
|
288
328
|
|
|
329
|
+
# ── Browser re-auth (device authorization flow) ─────────────────────────────
|
|
330
|
+
#
|
|
331
|
+
# `cinna login` refreshes the account token in place without a pasted setup
|
|
332
|
+
# token. It is an OAuth 2.0 Device Authorization Grant (RFC 8628): the CLL
|
|
333
|
+
# starts a request, the user authorizes it in a browser already signed in to the
|
|
334
|
+
# platform, and the CLI polls until the backend hands back a fresh account token.
|
|
335
|
+
#
|
|
336
|
+
# Backend contract (both unauthenticated — the point is the old token is dead):
|
|
337
|
+
# POST {platform}/api/v1/cli/account/login/start
|
|
338
|
+
# body: {machine_name, machine_info}
|
|
339
|
+
# 200 : {device_code, user_code, verification_uri,
|
|
340
|
+
# verification_uri_complete?, interval?, expires_in?}
|
|
341
|
+
# POST {platform}/api/v1/cli/account/login/poll
|
|
342
|
+
# body: {device_code}
|
|
343
|
+
# 200 : {status: "authorization_pending"|"slow_down"|"authorized"
|
|
344
|
+
# |"access_denied"|"expired_token",
|
|
345
|
+
# account_token?, platform_url?, frontend_url?, machine_name?}
|
|
346
|
+
|
|
347
|
+
_LOGIN_DEFAULT_INTERVAL = 5 # seconds between polls when the server omits one
|
|
348
|
+
_LOGIN_DEFAULT_EXPIRY = 900 # safety cap when the server omits expires_in
|
|
349
|
+
_LOGIN_START_PATH = "/api/v1/cli/account/login/start"
|
|
350
|
+
_LOGIN_POLL_PATH = "/api/v1/cli/account/login/poll"
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _login_start(platform_url: str, machine_name: str) -> dict:
|
|
354
|
+
"""Begin a device-login request; returns the authorize URL + device code."""
|
|
355
|
+
url = f"{platform_url.rstrip('/')}{_LOGIN_START_PATH}"
|
|
356
|
+
machine_info = f"{platform.system()}/{platform.machine()}"
|
|
357
|
+
logger.info("Starting device login at %s", url)
|
|
358
|
+
try:
|
|
359
|
+
response = httpx.post(
|
|
360
|
+
url,
|
|
361
|
+
json={"machine_name": machine_name, "machine_info": machine_info},
|
|
362
|
+
timeout=30.0,
|
|
363
|
+
)
|
|
364
|
+
except httpx.HTTPError as exc:
|
|
365
|
+
raise click.ClickException(f"Could not reach {platform_url}: {exc}")
|
|
366
|
+
if response.status_code == 404:
|
|
367
|
+
raise click.ClickException(
|
|
368
|
+
"This platform does not support 'cinna login' yet.\n"
|
|
369
|
+
"Refresh from the UI instead: open Settings → Local Development to "
|
|
370
|
+
"mint a new account setup token, then run\n"
|
|
371
|
+
" cinna account setup <token> (in the parent directory)."
|
|
372
|
+
)
|
|
373
|
+
if response.status_code != 200:
|
|
374
|
+
try:
|
|
375
|
+
detail = response.json().get("detail", response.text)
|
|
376
|
+
except Exception:
|
|
377
|
+
detail = response.text
|
|
378
|
+
raise click.ClickException(f"Login could not be started: {detail}")
|
|
379
|
+
return response.json()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _login_poll(platform_url: str, device_code: str) -> dict:
|
|
383
|
+
"""Poll a pending device-login request once."""
|
|
384
|
+
url = f"{platform_url.rstrip('/')}{_LOGIN_POLL_PATH}"
|
|
385
|
+
response = httpx.post(url, json={"device_code": device_code}, timeout=30.0)
|
|
386
|
+
if response.status_code != 200:
|
|
387
|
+
try:
|
|
388
|
+
detail = response.json().get("detail", response.text)
|
|
389
|
+
except Exception:
|
|
390
|
+
detail = response.text
|
|
391
|
+
raise click.ClickException(f"Login polling failed: {detail}")
|
|
392
|
+
return response.json()
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _poll_until_authorized(
|
|
396
|
+
platform_url: str, device_code: str, interval: int, expires_in: int
|
|
397
|
+
) -> dict:
|
|
398
|
+
"""Block until the user authorizes (or the request is denied / expires).
|
|
399
|
+
|
|
400
|
+
Honors the RFC 8628 ``slow_down`` backoff and the ``expires_in`` deadline.
|
|
401
|
+
Returns the authorized payload (carrying ``account_token``).
|
|
402
|
+
"""
|
|
403
|
+
deadline = time.monotonic() + expires_in
|
|
404
|
+
while time.monotonic() < deadline:
|
|
405
|
+
time.sleep(max(1, interval))
|
|
406
|
+
data = _login_poll(platform_url, device_code)
|
|
407
|
+
status = (data.get("status") or "").lower()
|
|
408
|
+
if status in ("authorized", "complete", "success"):
|
|
409
|
+
if not data.get("account_token"):
|
|
410
|
+
raise click.ClickException(
|
|
411
|
+
"Authorization succeeded but the server returned no token."
|
|
412
|
+
)
|
|
413
|
+
return data
|
|
414
|
+
if status in ("authorization_pending", "pending", ""):
|
|
415
|
+
continue
|
|
416
|
+
if status == "slow_down":
|
|
417
|
+
interval += 5
|
|
418
|
+
continue
|
|
419
|
+
if status in ("access_denied", "denied"):
|
|
420
|
+
raise click.ClickException("Authorization was denied in the browser.")
|
|
421
|
+
if status in ("expired_token", "expired"):
|
|
422
|
+
raise click.ClickException(
|
|
423
|
+
"The login request expired before you authorized it. "
|
|
424
|
+
"Run 'cinna login' again."
|
|
425
|
+
)
|
|
426
|
+
raise click.ClickException(f"Unexpected login status: {status!r}")
|
|
427
|
+
raise click.ClickException(
|
|
428
|
+
"Timed out waiting for authorization. Run 'cinna login' again."
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _device_login(platform_url: str, machine_name: str, frontend_url: str | None = None) -> dict:
|
|
433
|
+
"""Drive the full device-authorization handshake; return the authorized
|
|
434
|
+
payload.
|
|
435
|
+
|
|
436
|
+
Starts the request, surfaces the verification URL + user code (and opens a
|
|
437
|
+
browser), then polls until the user authorizes. The returned dict carries
|
|
438
|
+
``account_token`` plus any server-refreshed ``platform_url`` /
|
|
439
|
+
``frontend_url`` / ``machine_name``.
|
|
440
|
+
"""
|
|
441
|
+
console.status(f"Signing in to {frontend_url or platform_url} as {machine_name}…")
|
|
442
|
+
start = _login_start(platform_url, machine_name)
|
|
443
|
+
|
|
444
|
+
device_code = start.get("device_code")
|
|
445
|
+
if not device_code:
|
|
446
|
+
raise click.ClickException("Server did not return a device code.")
|
|
447
|
+
user_code = start.get("user_code") or ""
|
|
448
|
+
verify_url = (
|
|
449
|
+
start.get("verification_uri_complete")
|
|
450
|
+
or start.get("verification_url_complete")
|
|
451
|
+
or start.get("verification_uri")
|
|
452
|
+
or start.get("verification_url")
|
|
453
|
+
or start.get("verify_url")
|
|
454
|
+
)
|
|
455
|
+
if not verify_url:
|
|
456
|
+
raise click.ClickException("Server did not return an authorization URL.")
|
|
457
|
+
interval = int(start.get("interval") or _LOGIN_DEFAULT_INTERVAL)
|
|
458
|
+
expires_in = int(start.get("expires_in") or _LOGIN_DEFAULT_EXPIRY)
|
|
459
|
+
|
|
460
|
+
console.console.print()
|
|
461
|
+
if user_code:
|
|
462
|
+
console.console.print(f" Your verification code: [bold]{user_code}[/bold]")
|
|
463
|
+
console.console.print(" Open this URL and click Authorize:")
|
|
464
|
+
console.console.print(f" [bold]{verify_url}[/bold]")
|
|
465
|
+
console.console.print()
|
|
466
|
+
try:
|
|
467
|
+
webbrowser.open(verify_url)
|
|
468
|
+
except Exception:
|
|
469
|
+
pass # headless / no browser — the printed URL is the fallback.
|
|
470
|
+
|
|
471
|
+
with console.spinner("Waiting for authorization…"):
|
|
472
|
+
return _poll_until_authorized(platform_url, device_code, interval, expires_in)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _is_local_host(host: str) -> bool:
|
|
476
|
+
h = host.split(":")[0].lower()
|
|
477
|
+
return h in ("localhost", "127.0.0.1", "0.0.0.0", "::1") or h.endswith(".local")
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _normalize_platform_url(raw: str) -> str:
|
|
481
|
+
"""Turn a user-typed domain into a ``scheme://netloc`` platform URL.
|
|
482
|
+
|
|
483
|
+
Accepts ``app.example.com``, ``https://app.example.com/``,
|
|
484
|
+
``http://localhost:8000``, etc. A missing scheme defaults to ``https`` —
|
|
485
|
+
except for local hosts (``localhost`` / loopback / ``.local``), which get
|
|
486
|
+
``http``. Any path/query the user pasted is dropped.
|
|
487
|
+
"""
|
|
488
|
+
text = (raw or "").strip().strip("'\"").strip()
|
|
489
|
+
if not text:
|
|
490
|
+
raise click.ClickException("No domain provided.")
|
|
491
|
+
if "://" not in text:
|
|
492
|
+
host_part = text.split("/")[0]
|
|
493
|
+
scheme = "http" if _is_local_host(host_part) else "https"
|
|
494
|
+
text = f"{scheme}://{text}"
|
|
495
|
+
parsed = urlparse(text)
|
|
496
|
+
if not parsed.netloc:
|
|
497
|
+
raise click.ClickException(f"Could not parse a domain from {raw!r}.")
|
|
498
|
+
return f"{parsed.scheme}://{parsed.netloc}"
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# ``cinna.log`` is written into cwd by the CLI's own logging setup before this
|
|
502
|
+
# check runs, so a genuinely fresh folder still "contains" it — treat it (and
|
|
503
|
+
# OS cruft) as not counting toward emptiness.
|
|
504
|
+
_IGNORABLE_DIR_ENTRIES = {".DS_Store", "cinna.log"}
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _dir_is_empty(path: Path) -> bool:
|
|
508
|
+
"""True if ``path`` doesn't exist or holds nothing but ignorable cruft."""
|
|
509
|
+
if not path.exists():
|
|
510
|
+
return True
|
|
511
|
+
return all(child.name in _IGNORABLE_DIR_ENTRIES for child in path.iterdir())
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _refresh_account_token_in_place(account_root: Path) -> None:
|
|
515
|
+
"""Resume path: swap a fresh token into an existing account workspace."""
|
|
516
|
+
account_cfg = load_account_config(account_root)
|
|
517
|
+
result = _device_login(
|
|
518
|
+
account_cfg.platform_url, account_cfg.machine_name, account_cfg.frontend_url
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
account_cfg.account_token = result["account_token"]
|
|
522
|
+
if result.get("platform_url"):
|
|
523
|
+
account_cfg.platform_url = result["platform_url"]
|
|
524
|
+
if result.get("frontend_url"):
|
|
525
|
+
account_cfg.frontend_url = result["frontend_url"]
|
|
526
|
+
if result.get("machine_name"):
|
|
527
|
+
account_cfg.machine_name = result["machine_name"]
|
|
528
|
+
save_account_config(account_cfg, account_root)
|
|
529
|
+
|
|
530
|
+
console.status(
|
|
531
|
+
f"Signed in — account token refreshed for {account_cfg.machine_name}."
|
|
532
|
+
)
|
|
533
|
+
console.console.print(
|
|
534
|
+
" Re-mint expired sub-agent tokens with [bold]cinna doctor[/bold]."
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _login_new_account(
|
|
539
|
+
domain: str | None, machine_name: str, dir_name: str | None
|
|
540
|
+
) -> None:
|
|
541
|
+
"""Bootstrap path: connect a brand-new account workspace via the browser.
|
|
542
|
+
|
|
543
|
+
Prompts for the platform domain when not given, picks where to create the
|
|
544
|
+
workspace (the current folder when it's empty, otherwise a subfolder the
|
|
545
|
+
user names), runs the device-login flow against that domain, and
|
|
546
|
+
materializes a standard account workspace with the returned token.
|
|
547
|
+
"""
|
|
548
|
+
console.status("No cinna account workspace here — let's connect a new one.")
|
|
549
|
+
if not domain:
|
|
550
|
+
domain = click.prompt("Platform domain to log in to (e.g. app.example.com)")
|
|
551
|
+
platform_url = _normalize_platform_url(domain)
|
|
552
|
+
|
|
553
|
+
cwd = Path.cwd()
|
|
554
|
+
if dir_name:
|
|
555
|
+
account_root = cwd / dir_name
|
|
556
|
+
elif _dir_is_empty(cwd):
|
|
557
|
+
account_root = cwd
|
|
558
|
+
else:
|
|
559
|
+
default_sub = default_account_dir_name(platform_url)
|
|
560
|
+
sub = click.prompt(
|
|
561
|
+
"This folder isn't empty — name a subfolder to create the account "
|
|
562
|
+
"workspace in",
|
|
563
|
+
default=default_sub,
|
|
564
|
+
)
|
|
565
|
+
account_root = cwd / sub
|
|
566
|
+
|
|
567
|
+
if account_config_path(account_root).exists():
|
|
568
|
+
raise click.ClickException(
|
|
569
|
+
f"'{account_root}' already contains an account workspace.\n"
|
|
570
|
+
f"Run 'cinna login' from inside it to refresh its token."
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
result = _device_login(platform_url, machine_name)
|
|
574
|
+
|
|
575
|
+
config = AccountConfig(
|
|
576
|
+
platform_url=result.get("platform_url") or platform_url,
|
|
577
|
+
frontend_url=result.get("frontend_url") or platform_url,
|
|
578
|
+
account_token=result["account_token"],
|
|
579
|
+
machine_name=result.get("machine_name") or machine_name,
|
|
580
|
+
)
|
|
581
|
+
_write_account_files(config, account_root)
|
|
582
|
+
with console.spinner("Downloading context package…"):
|
|
583
|
+
_install_context_package(config, account_root)
|
|
584
|
+
|
|
585
|
+
rel = account_root if account_root == cwd else account_root.relative_to(cwd)
|
|
586
|
+
console.status(f"Account workspace ready at {account_root}")
|
|
587
|
+
console.console.print()
|
|
588
|
+
if account_root != cwd:
|
|
589
|
+
console.console.print(f" cd {rel}/")
|
|
590
|
+
console.console.print(
|
|
591
|
+
" cinna account agents # list agents you can build"
|
|
592
|
+
)
|
|
593
|
+
console.console.print(
|
|
594
|
+
" cinna agent sync <agent> # attach an agent workspace under agents/"
|
|
595
|
+
)
|
|
596
|
+
console.console.print()
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def run_login(
|
|
600
|
+
domain: str | None = None,
|
|
601
|
+
machine_name: str | None = None,
|
|
602
|
+
dir_name: str | None = None,
|
|
603
|
+
) -> None:
|
|
604
|
+
"""`cinna login` — resume an account workspace, or connect a new one.
|
|
605
|
+
|
|
606
|
+
Inside an existing account workspace it refreshes the stored token in place
|
|
607
|
+
(the ``domain`` / ``dir_name`` hints are ignored). Otherwise it bootstraps a
|
|
608
|
+
new account workspace: it asks for the platform domain (unless given),
|
|
609
|
+
creates the workspace in the current folder when empty — or in a named
|
|
610
|
+
subfolder when not — and signs in via the browser device flow. Either way no
|
|
611
|
+
setup token is pasted.
|
|
612
|
+
"""
|
|
613
|
+
try:
|
|
614
|
+
account_root = find_account_root()
|
|
615
|
+
except AccountConfigNotFoundError:
|
|
616
|
+
account_root = None
|
|
617
|
+
|
|
618
|
+
if account_root is not None:
|
|
619
|
+
if domain or dir_name:
|
|
620
|
+
console.warn(
|
|
621
|
+
"Already inside an account workspace — refreshing it in place "
|
|
622
|
+
"(domain / --dir ignored)."
|
|
623
|
+
)
|
|
624
|
+
_refresh_account_token_in_place(account_root)
|
|
625
|
+
return
|
|
626
|
+
|
|
627
|
+
_login_new_account(domain, machine_name or _fallback_machine_name(), dir_name)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _fallback_machine_name() -> str:
|
|
631
|
+
return f"{os.environ.get('USER', 'dev')}'s {platform.node()}"
|
|
632
|
+
|
|
633
|
+
|
|
289
634
|
# ── Command bodies ──────────────────────────────────────────────────────────
|
|
290
635
|
|
|
291
636
|
|
|
@@ -335,8 +680,13 @@ def _write_account_mcp_config(account_root: Path) -> None:
|
|
|
335
680
|
``POST /account/knowledge/search`` — the account analogue of the per-agent
|
|
336
681
|
workspace's knowledge tool. Auto-generated infra: overwritten on every
|
|
337
682
|
``cinna account setup`` / ``cinna account refresh-context``.
|
|
683
|
+
|
|
684
|
+
The config path is written **relative** to the account root (anchored at the
|
|
685
|
+
launch cwd, which MCP clients set to the workspace folder) so the folder can
|
|
686
|
+
be moved without breaking the proxy. ``run_mcp_proxy`` additionally walks up
|
|
687
|
+
from cwd, which self-heals older configs that stored an absolute path.
|
|
338
688
|
"""
|
|
339
|
-
account_config =
|
|
689
|
+
account_config = f"{CONFIG_DIR}/{ACCOUNT_CONFIG_FILE}"
|
|
340
690
|
|
|
341
691
|
mcp_json = {
|
|
342
692
|
"mcpServers": {
|
|
@@ -433,12 +783,40 @@ def _install_context_package(
|
|
|
433
783
|
return True
|
|
434
784
|
|
|
435
785
|
|
|
786
|
+
def _write_account_files(config: AccountConfig, account_root: Path) -> None:
|
|
787
|
+
"""Create the account workspace dir + config + generated files (no context).
|
|
788
|
+
|
|
789
|
+
The filesystem half of materializing an account workspace, shared by
|
|
790
|
+
``cinna account setup`` (paste a setup token) and ``cinna login`` (browser
|
|
791
|
+
device flow). The caller downloads the context package separately so each
|
|
792
|
+
can frame that slow, best-effort step in its own UI.
|
|
793
|
+
"""
|
|
794
|
+
account_root.mkdir(parents=True, exist_ok=True)
|
|
795
|
+
save_account_config(config, account_root)
|
|
796
|
+
agents_dir(account_root).mkdir(exist_ok=True)
|
|
797
|
+
_write_account_claude_md(account_root, config)
|
|
798
|
+
_write_account_claude_settings(account_root)
|
|
799
|
+
_write_account_mcp_config(account_root)
|
|
800
|
+
|
|
801
|
+
|
|
436
802
|
def run_account_setup(
|
|
437
|
-
setup_input: str, machine_name: str, dir_name: str =
|
|
803
|
+
setup_input: str, machine_name: str, dir_name: str | None = None
|
|
438
804
|
) -> None:
|
|
439
|
-
"""Full account setup flow — called by `cinna account setup <token_or_url>`.
|
|
805
|
+
"""Full account setup flow — called by `cinna account setup <token_or_url>`.
|
|
806
|
+
|
|
807
|
+
When ``dir_name`` is not given (no ``--dir``), the folder name defaults to
|
|
808
|
+
the platform domain normalized (e.g. ``demo-core_opencinna_io``); the user
|
|
809
|
+
can accept it or type their own at the prompt.
|
|
810
|
+
"""
|
|
440
811
|
total = 3
|
|
441
812
|
|
|
813
|
+
# Parse before touching the filesystem / network so we can derive the
|
|
814
|
+
# default folder name from the platform domain (and fail fast on bad input).
|
|
815
|
+
platform_url, token = parse_account_setup_input(setup_input)
|
|
816
|
+
|
|
817
|
+
if not dir_name:
|
|
818
|
+
dir_name = _prompt_account_dir(default_account_dir_name(platform_url))
|
|
819
|
+
|
|
442
820
|
# Guard the target directory before burning the single-use setup token.
|
|
443
821
|
account_root = Path.cwd() / dir_name
|
|
444
822
|
if account_config_path(account_root).exists():
|
|
@@ -449,25 +827,17 @@ def run_account_setup(
|
|
|
449
827
|
|
|
450
828
|
# Step 1: Exchange the account setup token
|
|
451
829
|
console.step(1, total, "Authenticating...")
|
|
452
|
-
platform_url, token = parse_account_setup_input(setup_input)
|
|
453
830
|
payload = _exchange_account_setup_token(platform_url, token, machine_name)
|
|
454
831
|
|
|
455
832
|
# Step 2: Materialize the account workspace
|
|
456
833
|
console.step(2, total, "Creating account workspace...")
|
|
457
|
-
account_root.mkdir(exist_ok=True)
|
|
458
|
-
|
|
459
834
|
config = AccountConfig(
|
|
460
835
|
platform_url=payload["platform_url"],
|
|
461
836
|
frontend_url=payload.get("frontend_url") or payload["platform_url"],
|
|
462
837
|
account_token=payload["account_token"],
|
|
463
838
|
machine_name=payload.get("machine_name") or machine_name,
|
|
464
839
|
)
|
|
465
|
-
|
|
466
|
-
agents_dir(account_root).mkdir(exist_ok=True)
|
|
467
|
-
|
|
468
|
-
_write_account_claude_md(account_root, config)
|
|
469
|
-
_write_account_claude_settings(account_root)
|
|
470
|
-
_write_account_mcp_config(account_root)
|
|
840
|
+
_write_account_files(config, account_root)
|
|
471
841
|
|
|
472
842
|
# Step 3: Context package (best-effort — setup succeeds without it)
|
|
473
843
|
console.step(3, total, "Downloading context package...")
|