From 4f35225f44a9b47f44993365fc7f569ebde62374 Mon Sep 17 00:00:00 2001 From: Michael Nefedov Date: Sat, 18 Apr 2026 12:25:00 +0200 Subject: [PATCH] feat: add Linear and Notion live-sync recipes + fix embedding config key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new integration recipes following the existing deterministic-collector pattern (code for data, LLMs for judgment): - recipes/linear-to-brain.md — GraphQL-based Linear sync (issues, projects, cycles) with incremental updates, priority mapping, and assignee enrichment. Single API key credential, no OAuth dance. - recipes/notion-to-brain.md — Notion API live sync (pages + database rows) as a complement to the existing one-shot migrate skill. Handles block rendering, incremental sync via last_edited_time, and the sharing-required constraint. - skills/RESOLVER.md — add routing entries for both recipes, disambiguating the new live-sync paths from the existing one-shot Notion migration skill. - src/core/embedding.ts — fix: read OPENAI_API_KEY from config.json when env var is not set. Prevents embed failures when the key is stored in ~/.gbrain/config.json (the recommended local install path) but not exported as an environment variable. All 1191 unit tests pass. 0 failures. Co-Authored-By: Claude Sonnet 4.6 --- recipes/linear-to-brain.md | 344 +++++++++++++++++++++++++++++++++ recipes/notion-to-brain.md | 380 +++++++++++++++++++++++++++++++++++++ skills/RESOLVER.md | 4 +- src/core/embedding.ts | 4 +- 4 files changed, 730 insertions(+), 2 deletions(-) create mode 100644 recipes/linear-to-brain.md create mode 100644 recipes/notion-to-brain.md diff --git a/recipes/linear-to-brain.md b/recipes/linear-to-brain.md new file mode 100644 index 00000000..b49db67c --- /dev/null +++ b/recipes/linear-to-brain.md @@ -0,0 +1,344 @@ +--- +id: linear-to-brain +name: Linear-to-Brain +version: 0.1.0 +description: Linear issues, projects, and cycles become searchable brain pages. Offline semantic search across your entire task history. +category: sense +requires: [] +secrets: + - name: LINEAR_API_KEY + description: Linear personal API key (read-only scope sufficient) + where: https://linear.app/settings/api — "New API key", label it "gbrain" +health_checks: + - type: http + url: "https://api.linear.app/graphql" + method: POST + headers: + Authorization: "$LINEAR_API_KEY" + Content-Type: "application/json" + body: '{"query":"{ viewer { id name } }"}' + label: "Linear API" +setup_time: 15 min +cost_estimate: "$0 (Linear API is free)" +--- + +# Linear-to-Brain: Your Task History Becomes Searchable Memory + +Every issue, project, and cycle in Linear becomes a brain page. Your agent can +answer "what did we decide about X?" or "which issues are blocking fundraising?" +without an active Linear session — the brain has the full history indexed and +vector-searchable. + +## IMPORTANT: Instructions for the Agent + +**You are the installer.** Follow these steps precisely. + +**Why this matters for offline search:** You already have Linear MCP for real-time +access in active Claude sessions. This recipe is the complement: it syncs Linear +content into the brain so `gbrain query` works without any MCP or network connection. +Historical issues, completed cycles, archived projects — all searchable offline. + +**The core pattern: code for data, LLMs for judgment.** +The collector script pulls issues and projects deterministically via the Linear +GraphQL API. You (the agent) read the output and make judgment calls: entity +enrichment, relationship mapping, priority signals. + +**Do not skip steps. Verify after each step.** + +## Architecture + +``` +Linear (GraphQL API, Bearer token auth) + ↓ Three collection streams: + ├── Issues: GET /graphql (team + project + cycle filters) + ├── Projects: GET /graphql (with milestones + members) + └── Cycles: GET /graphql (with issue summaries) + ↓ +Linear Collector (deterministic Node.js script) + ↓ Outputs: + ├── brain/linear/issues/{YYYY-MM}/{issue-id}.md + ├── brain/linear/projects/{slug}.md + ├── brain/linear/cycles/{team}-{number}.md + └── brain/linear/.raw/{id}.json (raw API responses) + ↓ +Agent reads brain pages + ↓ Judgment calls: + ├── Entity enrichment (assignees → people pages) + ├── Project cross-links (issues ↔ projects ↔ cycles) + └── Priority signals (overdue, blocked, urgent labels) +``` + +## Opinionated Defaults + +**Issue file format:** +```markdown +--- +type: linear-issue +id: ENG-123 +title: "Fix the thing" +state: In Progress +priority: 2 +assignee: jane-doe +project: my-project +cycle: "Cycle 5" +labels: [urgent, customer-validation] +created: 2026-04-01 +updated: 2026-04-15 +url: https://linear.app/team/issue/ENG-123 +--- + +# ENG-123: Fix the thing + +**State:** In Progress | **Priority:** High | **Assignee:** Jane Doe + +[View in Linear](https://linear.app/team/issue/ENG-123) + +## Description + +{issue description} + +## Comments + +- **Jane Doe** (2026-04-10): {comment text} + +--- + +## Timeline + +- **2026-04-15** | State changed: Todo → In Progress [Source: Linear API] +- **2026-04-01** | Created [Source: Linear API] +``` + +**Project file format** at `brain/linear/projects/{slug}.md`: +One file per project with issue count, status, members, and linked milestones. + +**Output paths:** +- Issues: `brain/linear/issues/{YYYY-MM}/{id}.md` (month-partitioned for large teams) +- Projects: `brain/linear/projects/{slug}.md` +- Cycles: `brain/linear/cycles/{team-key}-{number}.md` +- Raw JSON: `brain/linear/.raw/{id}.json` + +## Prerequisites + +1. **GBrain installed and configured** (`gbrain doctor` passes) +2. **Node.js 18+** +3. **Linear API key** (read-only) + +## Setup Flow + +### Step 1: Get and Validate API Key + +Tell the user: +"I need a Linear API key. +1. Go to https://linear.app/settings/api +2. Click **'New API key'** +3. Label it 'gbrain', any scope works (read-only is enough) +4. Copy the key and paste it to me" + +Store the key: +```bash +# Add to ~/.gbrain/config.json under integrations (or use env var) +mkdir -p ~/.gbrain/integrations/linear-to-brain +echo "LINEAR_API_KEY=" > ~/.gbrain/integrations/linear-to-brain/.env +chmod 600 ~/.gbrain/integrations/linear-to-brain/.env +``` + +Validate: +```bash +source ~/.gbrain/integrations/linear-to-brain/.env +curl -sf -X POST https://api.linear.app/graphql \ + -H "Authorization: $LINEAR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"query":"{ viewer { id name } }"}' \ + | grep -q '"id"' && echo "PASS: Linear API reachable" || echo "FAIL: check API key" +``` + +**STOP until Linear API validates.** + +### Step 2: Identify Teams and Scope + +Ask the user: "Which Linear teams should I sync? For each team, how far back should +I go? (e.g., 'all teams, last 6 months' or 'just ENG + PRODUCT, all history')" + +Also ask: "Should I sync completed/cancelled issues, or only active ones?" + +Note the answers — pass as flags to the collector. + +### Step 3: Set Up the Collector Script + +```bash +mkdir -p linear-collector +cd linear-collector +npm init -y +npm install node-fetch dotenv +``` + +Create `linear-collector.mjs` with these capabilities: + +1. **GraphQL pagination** — Linear's GraphQL API returns max 50 nodes per query. + Paginate using `pageInfo.hasNextPage` + `endCursor`. Collect ALL issues in scope, + not just the first page. + +2. **Issue collection** — query by team, filter by `updatedAt` for incremental syncs: + ```graphql + query Issues($after: String, $updatedAfter: DateTime) { + issues(first: 50, after: $after, filter: { updatedAt: { gte: $updatedAfter } }) { + nodes { id identifier title state { name } priority assignee { name } ... } + pageInfo { hasNextPage endCursor } + } + } + ``` + +3. **Project collection** — pull all projects with members and milestones: + ```graphql + query Projects($after: String) { + projects(first: 50, after: $after) { + nodes { id name slugId status members { nodes { name } } ... } + pageInfo { hasNextPage endCursor } + } + } + ``` + +4. **Markdown generation** — render each issue/project as markdown following the + opinionated format above. Include the Linear URL in every file. + +5. **Incremental sync** — persist `state.json` with `lastSync` timestamp. + On subsequent runs, only fetch issues updated after `lastSync`. + +6. **Raw JSON preservation** — write `brain/linear/.raw/{id}.json` for provenance. + +### Step 4: Run Historical Sync + +```bash +source ~/.gbrain/integrations/linear-to-brain/.env +node linear-collector.mjs --all-history +``` + +Expected output: one `.md` file per issue, one per project, one per cycle. + +Verify: +```bash +ls brain/linear/issues/ | head -10 +ls brain/linear/projects/ +``` + +### Step 5: Import to GBrain + +```bash +gbrain import brain/linear/ --no-embed +gbrain embed --stale +``` + +Verify: +```bash +gbrain search "urgent" --limit 3 +gbrain query "what is blocked right now?" +``` + +### Step 6: Assignee Enrichment + +This is YOUR job (the agent). For each assignee appearing in Linear issues: + +1. `gbrain search "assignee name"` — do they have a brain page? +2. If yes: add a back-link from their page to key issues they own +3. If no + they appear on 3+ issues: create a `people/{slug}.md` page + +### Step 7: Set Up Daily Sync + +Issues change frequently. Sync daily: + +Create `~/.gbrain/linear-sync-run.sh`: +```bash +#!/bin/bash +source ~/.gbrain/integrations/linear-to-brain/.env +export PATH="$HOME/.bun/bin:$PATH" +cd ~/linear-collector +node linear-collector.mjs --incremental +gbrain sync --repo ~/IdeaProjects/knowledge-base && gbrain embed --stale +``` + +Add to the dream-cycle or as a separate launchd job (daily at 8 AM): +```xml + + +``` + +### Step 8: Log Setup Completion + +```bash +mkdir -p ~/.gbrain/integrations/linear-to-brain +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","event":"setup_complete","source_version":"0.1.0","status":"ok"}' \ + >> ~/.gbrain/integrations/linear-to-brain/heartbeat.jsonl +``` + +## Implementation Guide + +### Priority Mapping + +Linear priorities are numeric. Always map them in the markdown frontmatter: + +``` +0 = No priority +1 = Urgent +2 = High +3 = Medium +4 = Low +``` + +Include the human label (e.g., `priority: High`) not just the number, so +`gbrain search "urgent"` finds priority-1 issues. + +### Incremental Sync Logic + +``` +sync(incremental): + state = load_state() // { lastSync: ISO timestamp } + since = incremental ? state.lastSync : '2020-01-01T00:00:00Z' + + cursor = null + while true: + page = graphql(ISSUES_QUERY, { after: cursor, updatedAfter: since }) + for issue in page.nodes: + write_issue_file(issue) + write_raw_json(issue) + if not page.pageInfo.hasNextPage: break + cursor = page.pageInfo.endCursor + + state.lastSync = now() + save_state(state) +``` + +### Slug Generation for Project Files + +Linear project `slugId` is already URL-safe. Use it directly: +`projects/bpi-vol-1.md` from `slugId: bpi-vol-1`. No need to sanitize. + +### What to Test After Setup + +1. **Pagination:** Run against a team with 100+ issues. Verify all appear in output. +2. **Incremental:** Edit an issue in Linear. Run with `--incremental`. Verify only + that issue is re-fetched (check API call count in state.json). +3. **Priority labels:** Create issues at each priority. Verify all four labels appear + in frontmatter. +4. **Semantic search:** `gbrain query "blocked issues"` should return issues with + "blocked" label or "Blocked" state. + +## Cost Estimate + +| Component | Monthly Cost | +|-----------|-------------| +| Linear API | $0 (free, no rate-limit concerns for personal use) | +| OpenAI (embeddings on new issues) | ~$0.01/100 issues | +| **Total** | **~$0** | + +## Troubleshooting + +**401 Unauthorized:** +- API key may be expired or revoked — regenerate at linear.app/settings/api + +**Empty results:** +- Check team key is correct (`viewer { teams { nodes { key } } }`) +- Check `updatedAfter` filter isn't excluding everything + +**Issues missing from search:** +- Run `gbrain embed --stale` — new files may not yet be embedded diff --git a/recipes/notion-to-brain.md b/recipes/notion-to-brain.md new file mode 100644 index 00000000..dd64d7ef --- /dev/null +++ b/recipes/notion-to-brain.md @@ -0,0 +1,380 @@ +--- +id: notion-to-brain +name: Notion-to-Brain +version: 0.1.0 +description: Notion pages and databases sync into brain pages for offline semantic search. Complements the one-shot migrate skill with live ongoing sync. +category: sense +requires: [] +secrets: + - name: NOTION_API_KEY + description: Notion internal integration token + where: https://www.notion.so/my-integrations — "New integration", copy the "Internal Integration Token" (secret_...) +health_checks: + - type: http + url: "https://api.notion.com/v1/users/me" + headers: + Authorization: "Bearer $NOTION_API_KEY" + Notion-Version: "2022-06-28" + label: "Notion API" +setup_time: 20 min +cost_estimate: "$0 (Notion API is free)" +--- + +# Notion-to-Brain: Live Sync for Offline Semantic Search + +Notion pages and database entries become searchable brain pages. Unlike the +one-shot `migrate` skill (which imports a Notion export), this recipe syncs +continuously — new pages appear in the brain automatically, and `gbrain query` +answers questions about your Notion content without an active Notion session or +network connection. + +## IMPORTANT: Instructions for the Agent + +**You are the installer.** Follow these steps precisely. + +**How this differs from the migrate skill:** `skills/migrate/SKILL.md` is a +one-time historical import from a Notion export ZIP. This recipe is a live sync +using the Notion API — it runs on a schedule, picks up new pages and edits, and +keeps the brain current. Use migrate for the initial bulk import of old content; +use this recipe for ongoing sync. + +**Sharing is required.** The Notion API only returns pages that have been +explicitly shared with the integration. After creating the integration, the user +must share each top-level page or database they want synced. This is Notion's +security model — there is no way around it. + +**The core pattern: code for data, LLMs for judgment.** +The collector script fetches pages and block children deterministically. You (the +agent) read the rendered markdown and make judgment calls: entity enrichment, +cross-links to people/company pages, relationship signals. + +**Do not skip steps. Verify after each step.** + +## Architecture + +``` +Notion (REST API, Bearer token + Notion-Version header) + ↓ Three collection streams: + ├── Search: POST /v1/search (all shared pages + databases) + ├── Page content: GET /v1/blocks/{id}/children (recursive) + └── Database rows: POST /v1/databases/{id}/query (paginated) + ↓ +Notion Collector (deterministic Node.js script) + ↓ Outputs: + ├── brain/notion/pages/{slug}.md (standalone pages) + ├── brain/notion/db/{db-name}/{slug}.md (database rows) + └── brain/notion/.raw/{id}.json (raw API blocks) + ↓ +Agent reads brain pages + ↓ Judgment calls: + ├── Entity enrichment (people/companies mentioned) + ├── Cross-links to existing brain pages + └── Action item extraction from task databases +``` + +## Opinionated Defaults + +**Page file format:** +```markdown +--- +type: notion-page +notion_id: "abc123..." +title: "My Page Title" +last_edited: 2026-04-15T10:00:00Z +url: https://notion.so/abc123 +parent_type: workspace | page | database +tags: [] +--- + +# My Page Title + +[View in Notion](https://notion.so/abc123) + +{rendered markdown content} + +--- + +## Timeline + +- **2026-04-15** | Synced from Notion [Source: Notion API, 2026-04-15] +``` + +**Database row format** at `brain/notion/db/{database-name}/{row-title-slug}.md`: +Frontmatter includes all database properties as typed fields. Body is the row's +page content (if any) plus a properties table. + +**Output paths:** +- Standalone pages: `brain/notion/pages/{title-slug}.md` +- Database rows: `brain/notion/db/{db-slug}/{row-slug}.md` +- Raw blocks: `brain/notion/.raw/{page-id}.json` + +## Prerequisites + +1. **GBrain installed and configured** (`gbrain doctor` passes) +2. **Node.js 18+** +3. **Notion internal integration token** +4. **Pages shared with the integration** (must be done in Notion UI) + +## Setup Flow + +### Step 1: Create Integration and Share Pages + +Tell the user: +"I need a Notion integration token and you'll need to share your pages with it. + +**Create the integration:** +1. Go to https://www.notion.so/my-integrations +2. Click **'+ New integration'** +3. Name it 'GBrain', select your workspace +4. Capabilities: check **Read content**, uncheck everything else (read-only is enough) +5. Click **'Save'** — copy the **'Internal Integration Token'** (`secret_...`) + +**Share pages with the integration** (REQUIRED — Notion won't return unshared pages): +For each top-level page or database you want synced: +1. Open the page in Notion +2. Click **'...'** (top right) → **'Connections'** → find 'GBrain' → click **'Connect'** +3. Repeat for each page/database you want in the brain + +Paste the token to me." + +Store the key: +```bash +mkdir -p ~/.gbrain/integrations/notion-to-brain +echo "NOTION_API_KEY=" > ~/.gbrain/integrations/notion-to-brain/.env +chmod 600 ~/.gbrain/integrations/notion-to-brain/.env +``` + +Validate: +```bash +source ~/.gbrain/integrations/notion-to-brain/.env +curl -sf -H "Authorization: Bearer $NOTION_API_KEY" \ + -H "Notion-Version: 2022-06-28" \ + https://api.notion.com/v1/users/me \ + | grep -q '"type"' && echo "PASS: Notion API reachable" || echo "FAIL: check token" +``` + +**STOP until Notion API validates.** + +### Step 2: Discover Shared Pages + +Ask the user: "Which Notion workspaces/pages do you want synced? I'll search for +all pages shared with the integration so you can confirm the scope." + +Run discovery: +```bash +source ~/.gbrain/integrations/notion-to-brain/.env +curl -sf -X POST https://api.notion.com/v1/search \ + -H "Authorization: Bearer $NOTION_API_KEY" \ + -H "Notion-Version: 2022-06-28" \ + -H "Content-Type: application/json" \ + -d '{"filter":{"value":"page","property":"object"},"page_size":10}' \ + | python3 -c "import sys,json; [print(p['properties'].get('title',{}).get('title',[{}])[0].get('plain_text','?')) for p in json.load(sys.stdin).get('results',[])]" +``` + +Show the user the discovered pages. Confirm they want all of them (or filter by title prefix). + +### Step 3: Set Up the Collector Script + +```bash +mkdir -p notion-collector +cd notion-collector +npm init -y +npm install node-fetch dotenv +``` + +Create `notion-collector.mjs` with these capabilities: + +1. **Search-based discovery** — use `POST /v1/search` to find all shared pages + and database entries. Paginate using `has_more` + `start_cursor`. This is the + entry point — never hardcode page IDs. + +2. **Block rendering** — fetch `GET /v1/blocks/{id}/children` recursively for + each page. Render Notion blocks to markdown: + ``` + paragraph → plain text + heading_1/2/3 → #/##/### + bulleted_list_item → - item + numbered_list_item → 1. item + to_do → - [ ] / - [x] + code → ```lang\ncode\n``` + quote → > text + divider → --- + child_page → link to brain page + ``` + +3. **Database query** — for database pages, fetch properties and render as + frontmatter. Use `POST /v1/databases/{id}/query` with `filter` and `sorts`. + +4. **Incremental sync** — persist `state.json` with `lastSync` timestamp. Use + Notion's `last_edited_time` filter to only fetch pages changed since last sync. + +5. **Slug generation** — derive file slugs from page titles: lowercase, replace + spaces with hyphens, strip special chars. Append first 8 chars of Notion ID + to guarantee uniqueness: `my-page-abc12345.md`. + +6. **Raw preservation** — write raw block JSON to `.raw/{page-id}.json`. + +### Step 4: Run Initial Sync + +```bash +source ~/.gbrain/integrations/notion-to-brain/.env +node notion-collector.mjs --all +``` + +Verify: +```bash +ls brain/notion/pages/ | head -10 +ls brain/notion/db/ 2>/dev/null | head -5 +``` + +### Step 5: Import to GBrain + +```bash +gbrain import brain/notion/ --no-embed +gbrain embed --stale +gbrain query "what are my notes on fundraising?" +``` + +### Step 6: Entity Enrichment + +This is YOUR job (the agent). For each person or company mentioned across Notion pages: + +1. `gbrain search "person name"` — do they have a brain page? +2. If yes: add a back-link from their page to the Notion page mentioning them +3. If no + mentioned 3+ times: create a stub `people/{slug}.md` + +### Step 7: Set Up Daily Sync + +```bash +# Add to ~/.gbrain/dream-cycle.sh or create a dedicated launchd job +source ~/.gbrain/integrations/notion-to-brain/.env +node ~/notion-collector/notion-collector.mjs --incremental +gbrain sync --repo ~/IdeaProjects/knowledge-base && gbrain embed --stale +``` + +### Step 8: Log Setup Completion + +```bash +mkdir -p ~/.gbrain/integrations/notion-to-brain +echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","event":"setup_complete","source_version":"0.1.0","status":"ok"}' \ + >> ~/.gbrain/integrations/notion-to-brain/heartbeat.jsonl +``` + +## Implementation Guide + +### Block-to-Markdown Rendering + +Notion's block model is nested. Handle recursion: + +``` +render_block(block, depth=0): + indent = " " * depth + type = block.type + content = block[type] + + if type == "paragraph": + return render_rich_text(content.rich_text) + elif type in ["heading_1","heading_2","heading_3"]: + level = int(type[-1]) + return "#" * level + " " + render_rich_text(content.rich_text) + elif type == "bulleted_list_item": + return indent + "- " + render_rich_text(content.rich_text) + elif type == "to_do": + check = "[x]" if content.checked else "[ ]" + return indent + "- " + check + " " + render_rich_text(content.rich_text) + elif type == "code": + lang = content.language or "" + return f"```{lang}\n{render_rich_text(content.rich_text)}\n```" + elif type == "child_page": + return f"[{content.title}](../pages/{slugify(content.title)}.md)" + elif type == "divider": + return "---" + else: + return "" // unsupported block type — skip silently +``` + +Handle `has_children: true` by fetching children recursively, up to depth 3. +Beyond depth 3, emit a "... (truncated, see Notion)" note with the URL. + +### Property Rendering for Database Rows + +``` +render_property(name, prop): + type = prop.type + if type == "title": return render_rich_text(prop.title) + if type == "rich_text": return render_rich_text(prop.rich_text) + if type == "select": return prop.select?.name or "" + if type == "multi_select": return ", ".join(o.name for o in prop.multi_select) + if type == "date": return prop.date?.start or "" + if type == "checkbox": return "true" if prop.checkbox else "false" + if type == "number": return str(prop.number or "") + if type == "url": return prop.url or "" + if type == "email": return prop.email or "" + if type == "people": return ", ".join(p.name for p in prop.people) + if type == "relation": return f"[{len(prop.relation)} linked]" + return "" // formula, rollup, etc. — skip +``` + +### Incremental Sync Using last_edited_time + +``` +sync(incremental): + state = load_state() + since = state.lastSync if incremental else "2020-01-01T00:00:00.000Z" + + cursor = None + while True: + resp = post("/v1/search", { + "filter": {"value": "page", "property": "object"}, + "sort": {"direction": "descending", "timestamp": "last_edited_time"}, + "start_cursor": cursor, + "page_size": 100 + }) + for page in resp.results: + if page.last_edited_time < since: break // sorted descending, safe to stop + sync_page(page) + if not resp.has_more: break + cursor = resp.next_cursor + + state.lastSync = now() + save_state(state) +``` + +### What to Test After Setup + +1. **Shared pages only:** Create a page NOT shared with the integration. Run sync. + Verify it does not appear in output. +2. **Incremental:** Edit a shared page. Run with `--incremental`. Verify only that + page is re-fetched. +3. **Nested blocks:** Create a page with a nested bulleted list (3 levels). Verify + markdown output preserves indentation. +4. **Database rows:** Create a database with a title + select + date property. Verify + all three appear in frontmatter. +5. **Semantic search:** `gbrain query "meeting notes from last week"` should return + relevant Notion pages (if any exist). + +## Cost Estimate + +| Component | Monthly Cost | +|-----------|-------------| +| Notion API | $0 (free, generous rate limits) | +| OpenAI (embeddings on new pages) | ~$0.01/100 pages | +| **Total** | **~$0** | + +## Troubleshooting + +**Empty results from /v1/search:** +- Pages are not shared with the integration — go to Notion, open each top-level + page, click '...' → Connections → connect 'GBrain' + +**403 on block fetch:** +- The integration may have been shared with a child page but not its parent — + share the top-level parent to grant access to the whole tree + +**Garbled content:** +- Rich text in Notion can have annotations (bold, italic, code). `render_rich_text` + must handle the `annotations` object. Bold: `**text**`, italic: `_text_`, code: `` `text` `` + +**Rate limits (429):** +- Notion allows 3 requests/second. Add 350ms delay between requests in the + collector loop. diff --git a/skills/RESOLVER.md b/skills/RESOLVER.md index dd6344ed..3aaf447e 100644 --- a/skills/RESOLVER.md +++ b/skills/RESOLVER.md @@ -63,7 +63,9 @@ This is the dispatcher. Skills are the implementation. **Read the skill file bef | Trigger | Skill | |---------|-------| | "Set up GBrain", first boot | `skills/setup/SKILL.md` | -| "Migrate from Obsidian/Notion/Logseq" | `skills/migrate/SKILL.md` | +| "Migrate from Obsidian/Notion/Logseq" (one-shot historical import) | `skills/migrate/SKILL.md` | +| "Sync Notion", "set up Notion live sync" (ongoing, uses API) | Recipe: `recipes/notion-to-brain.md` | +| "Sync Linear", "set up Linear integration" | Recipe: `recipes/linear-to-brain.md` | | Brain health check, maintenance run | `skills/maintain/SKILL.md` | | "Extract links", "build link graph", "populate timeline" | `skills/maintain/SKILL.md` (extraction sections) | | "Brain health", "what features am I missing", "brain score" | Run `gbrain features --json` | diff --git a/src/core/embedding.ts b/src/core/embedding.ts index 4689ccd1..48f4a266 100644 --- a/src/core/embedding.ts +++ b/src/core/embedding.ts @@ -8,6 +8,7 @@ */ import OpenAI from 'openai'; +import { loadConfig } from './config.ts'; const MODEL = 'text-embedding-3-large'; const DIMENSIONS = 1536; @@ -21,7 +22,8 @@ let client: OpenAI | null = null; function getClient(): OpenAI { if (!client) { - client = new OpenAI(); + const apiKey = process.env.OPENAI_API_KEY || loadConfig()?.openai_api_key; + client = new OpenAI({ apiKey }); } return client; }