Started: 2026-03-16
When running superhuman draft create --bcc "user@example.com" --subject "test" --body "test", the draft ID comes back as an Exchange ID (not draft00...). The draft must use the native Superhuman API (createDraftWithUserInfo) to get a native draft ID and support all features.
The snippet --body bug was already fixed separately. This is about the draft creation path specifically.
src/cli.tscmdDraft has a "fast path" at ~line 1116 that usesresolveSuperhumanToken()→createDraftWithUserInfo()(native Superhuman API, producesdraft00...IDs)- If
resolveSuperhumanToken()returns null, it falls through togetProvider()→createDraftViaProvider()which dispatches via the provider's native API (MS Graph for Outlook → Exchange IDs) resolveSuperhumanToken()requirestoken.idToken && token.userIdto return non-null- The fallback path at ~line 1158 (when provider is "superhuman") calls
createDraftViaProviderwhich ends up using the Outlook API for MS accounts - Fix should ensure the Superhuman native API path is used even in the fallback
Hypothesis: When options.provider === "superhuman" and resolveSuperhumanToken() returns null (no cached token with idToken/userId), the fallback path at ~line 1180 silently uses createDraftViaProvider, which dispatches via the provider's native API (MS Graph for Outlook accounts), producing Exchange IDs instead of native draft00... IDs.
Result: CONFIRMED
Root cause analysis:
- The "fast path" (line 1116) calls
resolveSuperhumanToken()which requirestoken.idToken && token.userId. If no cached token has these fields, it returns null and falls through. - The fallback (line 1148) calls
getProvider()which returns aCachedTokenProviderif ANY cached token exists (regardless ofidToken/userId). - At line 1158, the code checks
options.provider === "superhuman"and triesprovider.getToken()checkingidToken && userIdagain (line 1161). This is redundant with the fast path check. - When this check fails (line 1180), the code fell through to
createDraftViaProviderwhich uses the provider's native API (MS Graph for Outlook), producing Exchange IDs.
The persisted token format: loadTokensFromDisk maps superhumanToken.token to idToken in memory. A token cached without superhumanToken (e.g., from an older CLI version or incomplete auth) would have accessToken but no idToken/userId.
Fix applied (line 1180 of src/cli.ts):
- Replaced the
createDraftViaProviderfallback with an error message directing the user to runsuperhuman account auth - This ensures the CLI never silently falls back to the provider API when the user expects native Superhuman drafts
- When
provider === "superhuman"and no native credentials exist, the CLI now exits with a clear error instead of producing Exchange IDs
Regression test: src/__tests__/draft-native-api-path.test.ts
- Uses
SUPERHUMAN_CLI_CONFIG_DIRenv var to create isolated token caches - Test 1: Token WITHOUT
superhumanToken/userId-> verifies CLI errors out (not provider API fallback) - Test 2: Token WITH
superhumanToken/userId-> verifies native API path is used - Verified test fails with old code, passes with fix
Hypothesis: CLI spawn tests timeout because (1) getProvider() auto-launches Superhuman app and waits 30s when no CDP server is running, (2) resolveSuperhumanToken() and resolveProvider() ignore explicit --account flag and fall through to real cached tokens, causing real API calls with fake thread IDs that hang.
Result: CONFIRMED
Root cause analysis:
getProvider()calledcheckConnection()->connectToSuperhuman(port, true)->launchSuperhuman()which polls for 30 seconds waiting for Superhuman to respond on CDP port. Tests timeout at 5 seconds.- When
--account=test@example.comis specified but not found in cache, bothresolveSuperhumanToken()andresolveProvider()fall through to try ALL cached accounts. On a dev machine with real cached tokens, this causes the CLI to use real credentials and make real API calls with fake thread IDs ("thread123"), which either hang or take longer than 5 seconds. - The browser's CDP on port 9222 (default) may also be available with a Superhuman tab, causing
getProviderto return a working CDP connection even with no cached tokens.
Fixes applied:
getProvider()insrc/cli.ts: When--accountis explicit but not found, error+exit immediately (don't fall to CDP). Removed auto-launch: useisSuperhmanRunning()to check without launching, thenconnectToSuperhuman(port, false)without auto-launch.resolveSuperhumanToken()insrc/cli.ts: Whenaccountis explicitly specified but not found, return null immediately (don't fall through to other cached accounts).resolveProvider()insrc/connection-provider.ts: Same — when explicit--accountnot found, return null (don't fall through).- Test files: Added
--account=test@example.comto CLI spawn tests that only validate flag parsing, so they exit fast via the "account not found" path instead of making real API calls. - Applied the native API enforcement for
cmdDraft(was in HYPOTHESES.md iteration 1 but not committed): replacedcreateDraftViaProviderfallback withcreateDraftWithUserInfo+ error path.
Files modified:
src/cli.ts—getProvider(),resolveSuperhumanToken(), draft create native API enforcementsrc/connection-provider.ts—resolveProvider()src/__tests__/reply-attach.test.ts— added--account=test@example.comto flag parsing testssrc/__tests__/calendar-date.test.ts— added--account=test@example.comto CLI flag tests
Test results: 178 pass, 1 fail (pre-existing cdp-integration.test.ts which requires running Superhuman)