Skip to content

M1 PR3: thomas cloud login / whoami / sync / logout#30

Merged
thomas-supervisor merged 1 commit intomainfrom
m1-pr3-cloud-cli
May 5, 2026
Merged

M1 PR3: thomas cloud login / whoami / sync / logout#30
thomas-supervisor merged 1 commit intomainfrom
m1-pr3-cloud-cli

Conversation

@thomas-supervisor
Copy link
Copy Markdown
Collaborator

Wires the local thomas CLI to the thomas-cloud SaaS. Federated design — Claude Code (or any other agent driving thomas) never holds the SaaS API key directly; it goes through thomas cloud … verbs that own the device token in ~/.thomas/cloud.json.

CLI surface

thomas cloud login [--base-url <url>] [--label <name>]
thomas cloud whoami [--json]
thomas cloud sync [--json]
thomas cloud logout [--json]

login is interactive (no --json) — it prints a verification URL + 8-char user code, then long-polls /v1/devices/poll until the user approves in their browser. The other three speak the JSON envelope (CloudWhoamiData / CloudSyncData / CloudLogoutData).

THOMAS_CLOUD_BASE_URL env override + --base-url flag let you point at a local thomas-cloud (http://localhost:8000) for dev. Default is https://thomas.trustunknown.com.

What's in the box

src/cloud/
  types.ts       wire shapes: DeviceBegin/Poll, CloudIdentity, CloudSnapshot
  identity.ts    r/w ~/.thomas/cloud.json (0600; device token + workspace)
  cache.ts       r/w ~/.thomas/cloud-cache.json (last /v1/sync snapshot)
  client.ts      fetch wrapper: device-token auth, timeouts, error mapping
  device.ts      RFC 8628 begin + poll loop (respects server interval)
  sync.ts        one-shot /v1/sync → snapshot → cache + lastSyncAt

src/commands/cloud/
  login.ts       interactive: print user_code + URL, poll until approved
  logout.ts      local-only: clear cloud.json (server revoke = future PR)
  whoami.ts      local-only: render cloud.json contents
  sync.ts        emit CloudSyncData summary

SKILL.md         "Driving thomas-cloud (optional SaaS)" section

New error codes

E_CLOUD_NOT_LOGGED_IN, E_CLOUD_UNAUTHORIZED, E_CLOUD_UNREACHABLE, E_CLOUD_TIMEOUT — all documented in SKILL.md with remediation.

Tests

5 new tests in tests/cloud.test.ts against an in-process fake cloud server that mirrors the real apps/api endpoints:

  • happy path: login → whoami → sync → logout (single-use token, lastSyncAt updates, idempotent logout)
  • already-logged-in: second login attempt errors out without re-issuing
  • sync without login: E_CLOUD_NOT_LOGGED_IN
  • whoami without login: loggedIn: false (exit 0, no error)
  • 401 from server: E_CLOUD_UNAUTHORIZED (simulates revoked token)

256 / 256 tests pass, typecheck clean, build 184 KB.

Real-machine E2E

Drove the full flow against a running apps/api (the thomas-cloud repo's PR2):

  1. thomas cloud whoamiloggedIn: false
  2. thomas cloud login --base-url http://127.0.0.1:8000 → prints user_code, polls
  3. curl /v1/devices/_dev/approve (acting as web user via DEV_MODE bypass)
  4. login completes → ~/.thomas/cloud.json written with deviceToken + workspaceId ✓
  5. thomas cloud sync → empty snapshot stored in cloud-cache.json ✓
  6. thomas cloud whoamilastSyncAt populated ✓
  7. thomas cloud logoutwasLoggedIn: true, cloud.json deleted ✓
  8. thomas cloud logout (idempotent) → wasLoggedIn: false

Each /v1/sync request carried Authorization: Bearer <token> correctly.

Depends on

Out of scope

  • Server-side device revocation on logout (DELETE /v1/devices/{id} not yet on the cloud)
  • Background sync loop (daemon-side polling for policy/bundle updates) — added once cloud has policy data to push (M2)
  • Drain runs.jsonl upward (M3 — needs /v1/runs/batch on the cloud)

🤖 Generated with Claude Code

Federated CLI surface for the thomas-cloud SaaS. Per the design, Claude
Code drives thomas-cloud only via these commands — the device token
never leaks into Claude Code's context, the same SKILL.md semantics
(JSON envelope, error codes) extend to the cloud surface.

  src/cloud/
    types.ts      DeviceBegin/Poll wire shapes, CloudIdentity, CloudSnapshot
    identity.ts   r/w ~/.thomas/cloud.json (device token + workspace, 0600)
    cache.ts      r/w ~/.thomas/cloud-cache.json (last /v1/sync snapshot)
    client.ts     fetch wrapper: device-token auth, timeout, ThomasError
                  mapping (E_CLOUD_UNAUTHORIZED / _UNREACHABLE / _TIMEOUT)
    device.ts     RFC 8628-shaped begin + poll loop, respects server interval
    sync.ts       one-shot /v1/sync → snapshot → write cache + lastSyncAt

  src/commands/cloud/
    login.ts      interactive: prints user_code + URL, polls until approved
    logout.ts     local-only: clear cloud.json (server revoke = future PR)
    whoami.ts     local-only: render cloud.json contents (loggedIn?, ws, ...)
    sync.ts       drive sync.ts → emit summary

  src/cli.ts      cloud dispatch + help text (already in commit 2)
  src/config/paths.ts  paths.cloud + paths.cloudCache

Default base URL https://thomas.trustunknown.com. THOMAS_CLOUD_BASE_URL
env override (or --base-url flag on login) for local dev / private deploy.

login is the only non-JSON command (intrinsically interactive long-poll);
whoami / sync / logout all support --json with stable schemas
(CloudWhoamiData / CloudSyncData / CloudLogoutData declared in commit 2).

Tests: 5 cases against an in-process fake cloud server — happy path
(login → whoami → sync → logout), already-logged-in check, sync without
login, whoami when not logged in, sync against revoked token (401).

Real-machine E2E walkthrough: registered a user on thomas-cloud, drove
the full flow with the just-built CLI, verified cloud.json on disk +
Bearer auth on /v1/sync + lastSyncAt updates + idempotent logout.

SKILL.md adds "Driving thomas-cloud (optional SaaS)" section + new
E_CLOUD_* error codes in the troubleshooting table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thomas-supervisor thomas-supervisor merged commit 7512e61 into main May 5, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant