diff --git a/recipes/x-to-brain.md b/recipes/x-to-brain.md index 9392862a7..31c2d692d 100644 --- a/recipes/x-to-brain.md +++ b/recipes/x-to-brain.md @@ -1,12 +1,12 @@ --- id: x-to-brain name: X-to-Brain -version: 0.8.2 +version: 0.8.3 description: Twitter timeline, mentions, and keyword monitoring flow into brain pages. Tracks deletions, engagement velocity, OCR on images, and real-time alerts. category: sense requires: [] secrets: - - name: X_BEARER_TOKEN + - name: X_API_BEARER_TOKEN description: X API v2 Bearer token (Basic tier minimum, $200/mo for full archive search) where: https://developer.x.com/en/portal/dashboard — create a project + app, copy the Bearer Token from "Keys and tokens" - name: X_HANDLE @@ -16,7 +16,7 @@ health_checks: - type: http url: "https://api.x.com/2/users/by/username/$X_HANDLE" auth: bearer - auth_token: "$X_BEARER_TOKEN" + auth_token: "$X_API_BEARER_TOKEN" label: "X API" setup_time: 15 min cost_estimate: "$0-200/mo (Free tier: 1 app, read-only. Basic: $200/mo for search + higher limits)" @@ -118,11 +118,11 @@ Tell the user: Note: Free tier gives read-only access with low limits. Basic tier ($200/mo) gives search/recent endpoint and higher limits. Pro tier gets full archive search." -Set both `X_BEARER_TOKEN` and `X_HANDLE` in the environment. Validate immediately +Set both `X_API_BEARER_TOKEN` and `X_HANDLE` in the environment. Validate immediately (app-only bearer tokens cannot call `/users/me` — that endpoint requires user-context OAuth — so validation uses the by-username lookup): ```bash -curl -sf -H "Authorization: Bearer $X_BEARER_TOKEN" \ +curl -sf -H "Authorization: Bearer $X_API_BEARER_TOKEN" \ "https://api.x.com/2/users/by/username/$X_HANDLE" \ && echo "PASS: X API connected" \ || echo "FAIL: X API token invalid" @@ -138,7 +138,7 @@ starting with 'AAA...', (3) if you just created the app, the token is valid imme ```bash # Look up the user's X user ID from their handle -curl -sf -H "Authorization: Bearer $X_BEARER_TOKEN" \ +curl -sf -H "Authorization: Bearer $X_API_BEARER_TOKEN" \ "https://api.x.com/2/users/by/username/$X_HANDLE" | grep -o '"id":"[^"]*"' ``` @@ -210,7 +210,7 @@ The agent should review collected data 2-3x daily and run enrichment. ```bash mkdir -p ~/.gbrain/integrations/x-to-brain -echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","event":"setup_complete","source_version":"0.8.2","status":"ok","details":{"user_id":"X_USER_ID"}}' >> ~/.gbrain/integrations/x-to-brain/heartbeat.jsonl +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","event":"setup_complete","source_version":"0.8.3","status":"ok","details":{"user_id":"X_USER_ID"}}' >> ~/.gbrain/integrations/x-to-brain/heartbeat.jsonl ``` ## Production Patterns (v0.8.1) @@ -438,6 +438,14 @@ Free tier works for personal monitoring. Basic tier needed for keyword search. ## Troubleshooting +**Upgrading from recipe v0.8.2 or earlier (token shows [missing] after upgrade):** +- Older versions of this recipe named the token `X_BEARER_TOKEN`. The canonical + name is `X_API_BEARER_TOKEN` — the name the built-in `x_handle_to_tweet` + resolver reads. Rename the variable wherever you set it (shell profile, cron + environment, `.env`) — same value, new name. A collector installed under the + old name keeps running either way; the rename is what makes the integrations + dashboard and the resolver see the token. + **API returns 403:** - Check your app has the right access level (Read or Read+Write) - Free tier apps can only use basic endpoints diff --git a/src/commands/features.ts b/src/commands/features.ts index 33b21ef0e..c02a5740b 100644 --- a/src/commands/features.ts +++ b/src/commands/features.ts @@ -42,7 +42,7 @@ interface FeatureScanResult { const RECIPE_META = [ { id: 'email-to-brain', name: 'Email to Brain', secrets: ['GMAIL_APP_PASSWORD'] }, { id: 'calendar-to-brain', name: 'Calendar Sync', secrets: ['GOOGLE_CALENDAR_API_KEY'] }, - { id: 'x-to-brain', name: 'X/Twitter to Brain', secrets: ['X_BEARER_TOKEN'] }, + { id: 'x-to-brain', name: 'X/Twitter to Brain', secrets: ['X_API_BEARER_TOKEN'] }, { id: 'twilio-voice-brain', name: 'Voice to Brain', secrets: ['TWILIO_AUTH_TOKEN'] }, { id: 'meeting-sync', name: 'Meeting Sync', secrets: ['CIRCLEBACK_API_KEY'] }, { id: 'credential-gateway', name: 'Credential Gateway', secrets: ['OAUTH_CLIENT_SECRET'] }, diff --git a/test/features.test.ts b/test/features.test.ts index 6eb8168b3..4bdac5831 100644 --- a/test/features.test.ts +++ b/test/features.test.ts @@ -23,6 +23,52 @@ describe('recipe metadata', () => { }); }); +// #2789: the x-to-brain secret name must be the one the resolver actually +// reads. The recipe + RECIPE_META used to pin X_BEARER_TOKEN while the +// x_handle_to_tweet resolver reads only config x_api_bearer_token / env +// X_API_BEARER_TOKEN — so no single name worked end-to-end. All three +// surfaces must agree on the resolver's canonical name. +describe('x-to-brain secret name alignment (#2789)', () => { + const read = (p: string) => { + const { readFileSync } = require('fs'); + return readFileSync(new URL(p, import.meta.url), 'utf-8') as string; + }; + + it('features registry pins the resolver-canonical name', () => { + const src = read('../src/commands/features.ts'); + expect(src).toContain("{ id: 'x-to-brain', name: 'X/Twitter to Brain', secrets: ['X_API_BEARER_TOKEN'] }"); + }); + + it('the x-to-brain recipe declares and uses only the canonical name', async () => { + const { parseRecipe } = await import('../src/commands/integrations.ts'); + const raw = read('../recipes/x-to-brain.md'); + const recipe = parseRecipe(raw, 'x-to-brain.md'); + expect(recipe).not.toBeNull(); + // Frontmatter: the declared secret is the canonical name. + const secretNames = recipe!.frontmatter.secrets.map(s => s.name); + expect(secretNames).toContain('X_API_BEARER_TOKEN'); + expect(secretNames).not.toContain('X_BEARER_TOKEN'); + // Health check: the bearer interpolation uses the canonical name. + const hc = recipe!.frontmatter.health_checks[0] as { auth_token?: string }; + expect(hc.auth_token).toBe('$X_API_BEARER_TOKEN'); + // Body: every $-interpolated token reference (curl examples etc.) is the + // canonical name — catches a third misspelled variant, not just the exact + // legacy string. (The legacy name may still appear as PROSE in the + // upgrade/migration note; only $VAR references are load-bearing.) + const tokenRefs = raw.match(/\$X_[A-Z_]*TOKEN\b/g) ?? []; + expect(tokenRefs.length).toBeGreaterThan(0); + for (const ref of tokenRefs) expect(ref).toBe('$X_API_BEARER_TOKEN'); + }); + + it('the resolver reads the same env var the recipe documents', () => { + // Alignment guard (not a behavior test — resolver behavior is pinned in + // test/resolvers.test.ts): if the resolver's env name ever changes, this + // forces the recipe + registry to move with it. + const resolver = read('../src/core/resolvers/builtin/x-api/handle-to-tweet.ts'); + expect(resolver).toContain('process.env.X_API_BEARER_TOKEN'); + }); +}); + // Test brain_score in BrainHealth type describe('BrainHealth type', () => { it('includes brain_score field', async () => {