Implement the following plan:
traces sync command
Context
traces share pushes a trace to the server, but there's no way to pull one back down. When someone sees a trace ID in a git note or shared URL, they can view it in the browser but can't get it into their local ~/.traces/traces.db for the TUI. traces sync <externalId> closes that loop: fetch trace metadata + messages from the API, convert to CLI events, store locally.
Design decisions
Reuse existing GET endpoints, no new export route. GET /v1/traces/:id and GET /v1/traces/:id/messages?includeParts=true already return everything we need. Two sequential calls, no new API surface.
Reverse transform in the CLI. The CLI has buildMessagePayload (events -> API messages). We add the inverse: API messages+parts -> TraceEvents. Lives in api.ts alongside the forward transform.
Store as synced trace. The trace gets upserted into EventStore with the remote's externalId as the local trace ID (same convention as share). It won't have sourcePath or directory since those are local-only. sharedUrl gets set from the frontend URL so the TUI shows it as shared.
Changes
1. API client — cli/src/services/api.ts
Add two fetch methods and the reverse transform:
typescript
New types:
typescript
Reverse transform — buildEventFromMessage(msg: RemoteMessage, index: number): TraceEvent | null:
- Look at first part type to determine event kind
text + role user -> UserMessageEvent
text + role assistant -> AgentTextEvent
thinking -> AgentThinkingEvent
tool_call -> ToolCallEvent
tool_result -> ToolResultEvent
error -> ErrorEvent
- Fall back to textContent if no parts present
2. Sync command — cli/src/commands/sync.ts (new)
Follows the same runtime-injection pattern as share.ts and list.ts.
typescript
Flow:
- Validate externalId is provided
- Check API configured + authenticated
api.getTrace(externalId) — fetch metadata
api.getTraceMessages(externalId) — fetch messages with parts
- Convert messages to TraceEvents
- Build TraceMetadata from remote detail (agentId, title, timestamp, git fields, sharedUrl)
store.upsertTrace(metadata)
store.replaceEvents(externalId, events)
- Output success
3. Options parser — cli/src/commands/sync-options.ts (new)
Positional arg for externalId, --json flag.
4. CLI entry point — cli/src/index.ts
Add sync command to the dispatch chain.
5. Tests — cli/src/commands/sync.test.ts (new)
Using runtime injection to mock API and store:
- Missing externalId returns INVALID_ARGUMENTS error
- Successful sync upserts trace and replaces events
- JSON output format matches
{ ok, data: { traceId, messageCount, ... } }
- API 404 returns TRACE_NOT_FOUND error
- Unauthenticated returns AUTH_REQUIRED error
Files
cli/src/services/api.ts # getTrace, getTraceMessages, buildEventFromMessage, Api type
cli/src/commands/sync.ts # NEW — sync command
cli/src/commands/sync-options.ts # NEW — options parser
cli/src/commands/sync.test.ts # NEW — tests
cli/src/index.ts # command dispatch
Verification
cd cli && bun test — all tests pass
- Share a trace first:
traces share --trace-id <id> --json — note the externalId
traces sync <externalId> --json — confirm it stores and outputs correctly
traces list --json — confirm synced trace appears in the list
If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: /Users/andrew/.claude/projects/-Users-andrew-code-traces-traces/3784f20e-e940-4c28-9de5-1fc7cade0f35.jsonl