diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a73b07..fecd64b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,26 @@ # Changelog -All notable changes to iContext will be documented in this file. +All notable changes to fbrain will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [0.5.0] - 2026-05-06 + +### Changed +- Renamed the Python package, primary CLI, documentation, MCP server branding, and user-facing scripts from fbrain's former `icontext` name to `fbrain`. +- `fbrain` is now the canonical command and package name. +- Renamed canonical skills to `fbrain-populate-profile`, `fbrain-refresh-profile`, `fbrain-share-card`, and `fbrain-write-fact`. + +### Deprecated +- `icontext` remains available as a CLI shim for one release cycle. It prints a deprecation warning and forwards to `fbrain`; removal is planned for v0.6.0. +- `skills/icontext-*` remain as deprecated redirect stubs for one release cycle; removal is planned for v0.6.0. + ## [0.4.0] - 2026-05-04 ### Added -- **`icontext-write-fact` skill.** Explicit decision tree that routes content to the correct vault location before writing. Covers six categories: legal/entity facts (`vault/legal/`), project facts (`vault/projects/`), team/people (`vault/team/`), strategy (`vault/strategy/`), secretarial activity (`vault/secretary/logs/`), and credentials. Includes a full top-level vault directory reference so agents can match against the real structure without guessing. Eliminates the recurring "agent dumps legal facts in log files" failure mode. -- `icontext-populate-profile` now references `icontext-write-fact` for any durable facts surfaced during profile synthesis (e.g. company registrations, attorney contacts, funding dates). -- `icontext-write-fact` ships with `icontext init` — installed to `~/.claude/skills/` and `~/.cursor/rules/` alongside the existing three skills. +- **`fbrain-write-fact` skill.** Explicit decision tree that routes content to the correct vault location before writing. Covers six categories: legal/entity facts (`vault/legal/`), project facts (`vault/projects/`), team/people (`vault/team/`), strategy (`vault/strategy/`), secretarial activity (`vault/secretary/logs/`), and credentials. Includes a full top-level vault directory reference so agents can match against the real structure without guessing. Eliminates the recurring "agent dumps legal facts in log files" failure mode. +- `fbrain-populate-profile` now references `fbrain-write-fact` for any durable facts surfaced during profile synthesis (e.g. company registrations, attorney contacts, funding dates). +- `fbrain-write-fact` ships with `fbrain init` — installed to `~/.claude/skills/` and `~/.cursor/rules/` alongside the existing three skills. - CLAUDE.md snippet updated to list all four skills. - Tests for the new skill: frontmatter, decision tree category coverage, init installation, Cursor rule, skills-list count, and CLAUDE.md reference. @@ -20,11 +31,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added - **Multi-device sync.** Your vault now follows you across machines via a private git remote. - - `icontext push` — commit and push local changes to origin - - `icontext pull` — rebase against origin with autostash (last-writer-wins, surfaces conflicts) - - `icontext autosync start|stop|status` — background agent that runs `icontext push` every 60 seconds - - macOS: `launchd` at `~/Library/LaunchAgents/dev.icontext.autosync.plist` - - Linux: `systemd --user` timer at `~/.config/systemd/user/icontext-autosync.timer` + - `fbrain push` — commit and push local changes to origin + - `fbrain pull` — rebase against origin with autostash (last-writer-wins, surfaces conflicts) + - `fbrain autosync start|stop|status` — background agent that runs `fbrain push` every 60 seconds + - macOS: `launchd` at `~/Library/LaunchAgents/dev.fbrain.autosync.plist` + - Linux: `systemd --user` timer at `~/.config/systemd/user/fbrain-autosync.timer` - `user-prompt-submit` hook now pulls from origin in the background if a remote is configured, so Claude Code sees fresh context from other machines at the start of every prompt. - CLAUDE.md snippet references the multi-device sync workflow. - Tests for `push`, `pull`, and `autosync` (--help, no-origin hint, status reports not running on clean home). @@ -37,7 +48,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed - `cli.py` syntax error in `cmd_init` final message (escaped quote in f-string broke fresh installs) - Doctor now recognizes skills-first install mode and downgrades legacy-mode checks to warns -- Doctor checks connector files at install root (`~/icontext/connectors/`), not inside vault +- Doctor checks connector files at install root (`~/fbrain/connectors/`), not inside vault - Doctor `gitleaks` invocation uses `detect` subcommand (not the invalid `dir`) ### Added @@ -45,29 +56,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - CHANGELOG.md (Keep a Changelog format) ### Changed -- Skill `icontext-populate-profile`: tiered relationship threshold, project-vs-task distinction, explicit date instruction (raised dogfooded quality from 6.5/10 to 8/10) +- Skill `fbrain-populate-profile`: tiered relationship threshold, project-vs-task distinction, explicit date instruction (raised dogfooded quality from 6.5/10 to 8/10) ## [0.2.0] - 2026-05-02 ### Added -- Skills-first architecture: `icontext init` installs three skills into `~/.claude/skills/` and `~/.cursor/rules/` - - `icontext-populate-profile`: agent-driven profile synthesis with cascade (Gmail MCP -> LinkedIn -> user-described) - - `icontext-refresh-profile`: staleness handling for keeping profiles current - - `icontext-share-card`: shareable summary card generation +- Skills-first architecture: `fbrain init` installs three skills into `~/.claude/skills/` and `~/.cursor/rules/` + - `fbrain-populate-profile`: agent-driven profile synthesis with cascade (Gmail MCP -> LinkedIn -> user-described) + - `fbrain-refresh-profile`: staleness handling for keeping profiles current + - `fbrain-share-card`: shareable summary card generation - Modular profile output split across `user.md`, `relationships.md`, `projects.md`, `context-card.md` -- `icontext skills` subcommand (list, update) -- `icontext --version` flag +- `fbrain skills` subcommand (list, update) +- `fbrain --version` flag - Comparisons section in README covering mem0, OpenMemory, Obsidian, Pieces, CLAUDE.md, and Cursor Rules - Demo GIF in README - Threat model documented in SECURITY.md - 80 tests across connectors, CLI, and doctor - Polished CLI output with color and structure -- Copy-pasteable Claude prompt printed after `icontext init` +- Copy-pasteable Claude prompt printed after `fbrain init` - Demo script and README quickstart ### Changed -- `icontext init` no longer requires a Gemini API key -- `icontext sync` (Gemini-driven) is now an optional headless fallback (`pip install icontext[sync]`) +- `fbrain init` no longer requires a Gemini API key +- `fbrain sync` (Gemini-driven) is now an optional headless fallback (`pip install fbrain[sync]`) - Synthesis pipeline rewritten as 3-stage architecture (extract -> validate -> render), used by optional sync - README hero rewritten around "Your AI agents now share a brain" - Replaced ai-sidecar dependency with Gemini SDK; credentials now stored via OS keychain @@ -89,4 +100,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [0.1.0] - 2026-04-26 -Initial public release. Vault structure with sensitivity tier checks, MCP context index server, basic Gmail and LinkedIn connectors, icontext CLI, prompt hook for Codex/Cursor/OpenCode, and CI secret scanning via gitleaks. +Initial public release. Vault structure with sensitivity tier checks, MCP context index server, basic Gmail and LinkedIn connectors, fbrain CLI, prompt hook for Codex/Cursor/OpenCode, and CI secret scanning via gitleaks. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ac86686..00da033 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Thanks for your interest in iContext. +Thanks for your interest in fbrain. ## Reporting issues @@ -19,8 +19,8 @@ Thanks for your interest in iContext. ## Local development ```bash -git clone https://github.com/floomhq/icontext ~/icontext-dev -cd ~/icontext-dev +git clone https://github.com/floomhq/fbrain ~/fbrain-dev +cd ~/fbrain-dev pip install -e . pip install pytest pytest tests/ diff --git a/README.md b/README.md index 487c863..be8c3a3 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,37 @@ -# icontext +# fbrain -[![CI](https://github.com/floomhq/icontext/actions/workflows/ci.yml/badge.svg)](https://github.com/floomhq/icontext/actions/workflows/ci.yml) +[![CI](https://github.com/floomhq/fbrain/actions/workflows/ci.yml/badge.svg)](https://github.com/floomhq/fbrain/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/) -[![GitHub stars](https://img.shields.io/github/stars/floomhq/icontext)](https://github.com/floomhq/icontext/stargazers) +[![GitHub stars](https://img.shields.io/github/stars/floomhq/fbrain)](https://github.com/floomhq/fbrain/stargazers) -![icontext demo](demo/icontext-demo.gif) +![fbrain demo](demo/icontext-demo.gif) **Your AI agents now share a brain.** -icontext is a folder + a set of skills. Your AI tools (Claude Code, Cursor, Codex) read from it before answering, and write to it as they learn about you. Local. Encrypted. No API keys. +fbrain is a folder + a set of skills. Your AI tools (Claude Code, Cursor, Codex) read from it before answering, and write to it as they learn about you. Local. Encrypted. No API keys. ## Quickstart +```bash +curl -fsSL https://raw.githubusercontent.com/floomhq/fbrain/main/get.sh | bash +fbrain init +``` + +Deprecated one-release alias, retained for existing install docs: + ```bash curl -fsSL https://raw.githubusercontent.com/floomhq/icontext/main/get.sh | bash icontext init ``` -Open Claude Code and say: **"Populate my icontext profile."** +Open Claude Code and say: **"Populate my fbrain profile."** That's it. Your AI now has persistent memory. ## How it works -iContext is intentionally minimal infrastructure. Three pieces: +fbrain is intentionally minimal infrastructure. Three pieces: ### 1. The vault — a structured folder @@ -45,17 +52,18 @@ Every file is plain Markdown. Open it in a text editor, in Obsidian, or pipe it ### 2. Skills — instructions your AI agent follows -`icontext init` installs three skills: +`fbrain init` installs four skills: -- **icontext-populate-profile** — builds the profile from real data sources -- **icontext-refresh-profile** — updates a stale profile -- **icontext-share-card** — regenerates the shareable summary +- **fbrain-populate-profile** — builds the profile from real data sources +- **fbrain-refresh-profile** — updates a stale profile +- **fbrain-share-card** — regenerates the shareable summary +- **fbrain-write-fact** — routes durable facts to the right vault location Skills are Markdown files installed at: -- `~/.claude/skills/icontext-*/SKILL.md` (Claude Code) -- `~/.cursor/rules/icontext-*.mdc` (Cursor) +- `~/.claude/skills/fbrain-*/SKILL.md` (Claude Code) +- `~/.cursor/rules/fbrain-*.mdc` (Cursor) -When you ask Claude Code "populate my icontext profile", it discovers the skill via its description, reads the instructions, and executes them. +When you ask Claude Code "populate my fbrain profile", it discovers the skill via its description, reads the instructions, and executes them. ### 3. The synthesis pipeline @@ -77,22 +85,22 @@ The agent extracts structured data from the source: people (with evidence_messag **Stage D — Markdown rendering:** Write four files (`user.md`, `relationships.md`, `projects.md`, `context-card.md`) using a fixed template. Roles and context columns are required (cite subject evidence, never blank). No HTML comment delimiters or fragile parsing. -This is the same architecture as the optional headless `icontext sync` (which uses Gemini 2.5 Flash Lite directly), just executed by the user's AI agent instead. +This is the same architecture as the optional headless `fbrain sync` (which uses Gemini 2.5 Flash Lite directly), just executed by the user's AI agent instead. ### Cross-tool: every agent reads the same folder | Tool | How it reads | How it writes | |---|---|---| | Claude Code | CLAUDE.md snippet + skill files | Skill file invocation | -| Cursor | `.cursor/rules/icontext-*.mdc` | Same skill instructions, Cursor-flavored | -| Codex | reads vault directly (plain MD) | optional — `icontext sync` | -| OpenCode | reads vault directly (plain MD) | optional — `icontext sync` | +| Cursor | `.cursor/rules/fbrain-*.mdc` | Same skill instructions, Cursor-flavored | +| Codex | reads vault directly (plain MD) | optional — `fbrain sync` | +| OpenCode | reads vault directly (plain MD) | optional — `fbrain sync` | Any tool that can read Markdown can read the vault. Any tool that can follow Markdown instructions can populate it. ### Privacy and security -- **No external API calls by default.** Synthesis happens inside your AI agent's session. No iContext server, no telemetry, no profile leaves your machine. +- **No external API calls by default.** Synthesis happens inside your AI agent's session. No fbrain server, no telemetry, no profile leaves your machine. - **Credentials in OS keychain.** Gmail App Passwords (only used for the optional headless `sync`) are stored via `keyring` — macOS Keychain, Linux Secret Service. Never in plaintext JSON. - **Vault tier encryption.** `vault/` is git-crypt encrypted at rest. `internal/` and `shareable/` are plaintext (designed to be portable and readable in Obsidian). - **Pre-commit secret scanning.** `gitleaks` runs on every commit if you push the vault to git. @@ -127,12 +135,12 @@ GitHub → gitleaks + tier CI Synthesis runs inside your AI agent's session, not on a server. Default install requires no API keys. Email metadata never leaves your laptop. -icontext stores: +fbrain stores: - Your synthesized profile in `~/context/internal/profile/`. Plaintext on disk. Use FileVault. - Your vault secrets in `~/context/vault/`. Encrypted with `git-crypt`. -icontext does not run a server. No data is ever sent to any icontext-controlled endpoint. +fbrain does not run a server. No data is ever sent to any fbrain-controlled endpoint. Full threat model: see [SECURITY.md](SECURITY.md). @@ -141,17 +149,17 @@ Full threat model: see [SECURITY.md](SECURITY.md). If you don't have Claude Code or Cursor and want a fully automated pipeline, the original Gemini-based sync is still available: ```bash -pip install icontext[sync] -icontext connect gmail -icontext connect linkedin --pdf ~/Downloads/Profile.pdf -icontext sync +pip install fbrain[sync] +fbrain connect gmail +fbrain connect linkedin --pdf ~/Downloads/Profile.pdf +fbrain sync ``` This requires `GEMINI_API_KEY` and runs the same 3-stage synthesis as the agent skill, but headlessly. Use it for CI, scripts, or non-agent environments. ## Features -| Layer | What icontext does | +| Layer | What fbrain does | |---|---| | Skills | Markdown instructions your agent follows to populate and refresh your profile. | | Encryption | `vault/**` is protected with `git-crypt` in Git and on GitHub. | @@ -176,6 +184,13 @@ The classifier enforces content matches folder. Secrets are never allowed anywhe ## Install +```bash +curl -fsSL https://raw.githubusercontent.com/floomhq/fbrain/main/get.sh | bash +fbrain init +``` + +Deprecated alias for this release: + ```bash curl -fsSL https://raw.githubusercontent.com/floomhq/icontext/main/get.sh | bash icontext init @@ -184,24 +199,24 @@ icontext init Or manually: ```bash -git clone https://github.com/floomhq/icontext ~/icontext -pip install -e ~/icontext -icontext init +git clone https://github.com/floomhq/fbrain ~/fbrain +pip install -e ~/fbrain +fbrain init ``` -`icontext init` creates the vault, installs skills into `~/.claude/skills/` and `~/.cursor/rules/`, and adds a CLAUDE.md snippet so your agent loads the profile at session start. +`fbrain init` creates the vault, installs skills into `~/.claude/skills/` and `~/.cursor/rules/`, and adds a CLAUDE.md snippet so your agent loads the profile at session start. ### Skill management ```bash -icontext skills list # show installed skills and where they live -icontext skills update # pull latest skill versions from the icontext repo +fbrain skills list # show installed skills and where they live +fbrain skills update # pull latest skill versions from the fbrain repo ``` ## Prove it works ```bash -icontext doctor +fbrain doctor ``` The doctor command validates your install without starting background services or adding hosted dependencies. @@ -209,10 +224,10 @@ The doctor command validates your install without starting background services o ## Uninstall ```bash -bash ~/icontext/uninstall.sh /path/to/vault +bash ~/fbrain/uninstall.sh /path/to/vault ``` -Uninstall removes icontext-managed hooks, `.icontext/`, the GitHub Actions workflow, and the install manifest. It leaves your `shareable/`, `internal/`, and `vault/` content in place. +Uninstall removes fbrain-managed hooks, `.icontext/`, the GitHub Actions workflow, and the install manifest. It leaves your `shareable/`, `internal/`, and `vault/` content in place. ## Requirements @@ -224,11 +239,11 @@ Optional: - `gitleaks` for secret scanning (`brew install gitleaks`) - `git-crypt` for vault encryption (`brew install git-crypt`) - `git-lfs` for binary assets (`brew install git-lfs`) -- `GEMINI_API_KEY` only if you want the headless `icontext sync` fallback +- `GEMINI_API_KEY` only if you want the headless `fbrain sync` fallback ## Multi-device sync -Same vault, every machine. iContext uses git on a private repo as the sync layer — no extra service, no daemon talking to a vendor. +Same vault, every machine. fbrain uses git on a private repo as the sync layer — no extra service, no daemon talking to a vendor. ### Setup (3 steps) @@ -237,14 +252,14 @@ On your **primary machine**, push the vault to a private repo: ```bash cd ~/context gh repo create /context --private --source=. --push -icontext autosync start # commits + pushes every 60s +fbrain autosync start # commits + pushes every 60s ``` On every **other machine**, clone and start autosync: ```bash gh repo clone /context ~/context -icontext autosync start +fbrain autosync start ``` That's it. Edits made on machine A appear on machine B within ~60s of the next prompt (the `user-prompt-submit` hook also pulls in the background whenever you start a Claude Code prompt). @@ -253,44 +268,44 @@ That's it. Edits made on machine A appear on machine B within ~60s of the next p | Command | What it does | |---|---| -| `icontext push` | Commit local changes and push to origin | -| `icontext pull` | Rebase against origin (autostashes in-flight changes) | -| `icontext autosync start` | Install + start the 60s background agent | -| `icontext autosync stop` | Stop and remove the agent | -| `icontext autosync status` | Show running state and last sync time | +| `fbrain push` | Commit local changes and push to origin | +| `fbrain pull` | Rebase against origin (autostashes in-flight changes) | +| `fbrain autosync start` | Install + start the 60s background agent | +| `fbrain autosync stop` | Stop and remove the agent | +| `fbrain autosync status` | Show running state and last sync time | ### Conflict handling -`icontext pull` runs `git pull --rebase --autostash`. Last writer wins. If two machines edit the same lines of the same file in the same minute, the rebase surfaces the conflict and prints the file paths — resolve manually with normal git tooling. +`fbrain pull` runs `git pull --rebase --autostash`. Last writer wins. If two machines edit the same lines of the same file in the same minute, the rebase surfaces the conflict and prints the file paths — resolve manually with normal git tooling. In practice, conflicts are rare: profile files are append-mostly, and the 60s push window is shorter than typical edit cycles. ### Implementation -- macOS: `launchd` agent at `~/Library/LaunchAgents/dev.icontext.autosync.plist`. Logs at `~/Library/Logs/icontext.log`. -- Linux: `systemd --user` timer at `~/.config/systemd/user/icontext-autosync.timer`. Logs via `journalctl --user -u icontext-autosync.service`. +- macOS: `launchd` agent at `~/Library/LaunchAgents/dev.fbrain.autosync.plist`. Logs at `~/Library/Logs/fbrain.log`. +- Linux: `systemd --user` timer at `~/.config/systemd/user/fbrain-autosync.timer`. Logs via `journalctl --user -u fbrain-autosync.service`. -## How icontext compares +## How fbrain compares Common question: "isn't this just like X?" -| | What it is | How icontext is different | +| | What it is | How fbrain is different | |---|---|---| -| **mem0 / Letta / Zep** | Memory libraries for developers building agents | icontext is for end users; you don't write code to use it | -| **OpenMemory** | Local CLI + MCP for AI memory | OpenMemory's memory is reactive (built from chat history). icontext is proactive (built from your real data: Gmail, LinkedIn) | -| **Obsidian** | Knowledge base for humans | Obsidian is for humans writing notes; icontext is for AI agents writing context. Same folder works for both — open ~/context in Obsidian for the human view. | -| **Pieces.app** | OS-level capture for developers | Pieces captures what you do; icontext synthesizes who you are. Different layer. | -| **Claude Code's CLAUDE.md** | Per-project AI instructions | CLAUDE.md is per-project. icontext is your *identity* — the same context every project uses. | -| **Cursor Rules / .cursorrules** | Cursor-specific instructions | icontext works across Claude Code, Cursor, Codex, OpenCode via MCP and shared file conventions. Tool-agnostic. | +| **mem0 / Letta / Zep** | Memory libraries for developers building agents | fbrain is for end users; you don't write code to use it | +| **OpenMemory** | Local CLI + MCP for AI memory | OpenMemory's memory is reactive (built from chat history). fbrain is proactive (built from your real data: Gmail, LinkedIn) | +| **Obsidian** | Knowledge base for humans | Obsidian is for humans writing notes; fbrain is for AI agents writing context. Same folder works for both — open ~/context in Obsidian for the human view. | +| **Pieces.app** | OS-level capture for developers | Pieces captures what you do; fbrain synthesizes who you are. Different layer. | +| **Claude Code's CLAUDE.md** | Per-project AI instructions | CLAUDE.md is per-project. fbrain is your *identity* — the same context every project uses. | +| **Cursor Rules / .cursorrules** | Cursor-specific instructions | fbrain works across Claude Code, Cursor, Codex, OpenCode via MCP and shared file conventions. Tool-agnostic. | -The wedge: **icontext is the only tool that proactively builds your professional identity from sources you already own (Gmail, LinkedIn) and exposes it to every AI tool you use.** +The wedge: **fbrain is the only tool that proactively builds your professional identity from sources you already own (Gmail, LinkedIn) and exposes it to every AI tool you use.** ## Status -Production-ready. Run `icontext doctor` to verify your install. +Production-ready. Run `fbrain doctor` to verify your install. > Social preview image at `assets/og-image.png` — upload via Settings → Social preview -## Built with icontext +## Built with fbrain -*Share your setup: tag #icontext on Twitter/X* +*Share your setup: tag #fbrain on Twitter/X* diff --git a/SECURITY.md b/SECURITY.md index d159f7a..51c67b4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,16 +2,16 @@ ## Threat model -iContext stores sensitive personal data — email metadata, professional profile, synthesized identity. This document explains what we protect against, what we don't, and what you should know. +fbrain stores sensitive personal data — email metadata, professional profile, synthesized identity. This document explains what we protect against, what we don't, and what you should know. -### What iContext protects against +### What fbrain protects against -- **Cloud breach**: Your data is never sent to any iContext server. There is no iContext server. +- **Cloud breach**: Your data is never sent to any fbrain server. There is no fbrain server. - **Synthesis provider breach**: Only the synthesized profile is sent to Gemini for processing. Raw email contents are never sent. - **Repo leak (vault tier)**: Files in `vault/` are encrypted with git-crypt before commit. A leaked Git remote does not expose vault contents. - **Pre-commit secret leak**: `gitleaks` runs as a pre-commit hook to catch credentials. -### What iContext does NOT protect against +### What fbrain does NOT protect against - **Local machine compromise**: If your machine is compromised, all unencrypted vault tiers (`shareable/`, `internal/`) are accessible. Use full-disk encryption (FileVault on macOS). - **Repo leak (other tiers)**: Files in `shareable/` and `internal/` are NOT encrypted in Git. Don't push your vault to a public repo. @@ -44,11 +44,11 @@ iContext stores sensitive personal data — email metadata, professional profile | Destination | What | When | |---|---|---| -| imap.gmail.com | Your Gmail credentials, your IMAP queries | During `icontext sync gmail` | -| generativelanguage.googleapis.com | Synthesized email summary (no raw content), LinkedIn PDF text | During `icontext sync` | +| imap.gmail.com | Your Gmail credentials, your IMAP queries | During `fbrain sync gmail` | +| generativelanguage.googleapis.com | Synthesized email summary (no raw content), LinkedIn PDF text | During `fbrain sync` | | Nothing else | — | — | -iContext makes no other network requests. +fbrain makes no other network requests. ## Installer trust model @@ -64,10 +64,10 @@ Install modes narrow the trust surface: - `agents` also updates selected local agent configs so those tools can call the local MCP server. -The manifest records icontext-managed files with repo-relative paths and hashes +The manifest records fbrain-managed files with repo-relative paths and hashes for audit and uninstall. It intentionally avoids absolute local home paths. Run `uninstall.sh --dry-run` to preview the manifest removals. Uninstall removes -icontext-managed hooks, runtime files, root config, workflow, and manifest; it +fbrain-managed hooks, runtime files, root config, workflow, and manifest; it does not delete vault content. Agent installation does not grant a remote service new direct access by itself. diff --git a/cli.py b/cli.py index 7a70fd9..9e1a9c7 100755 --- a/cli.py +++ b/cli.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""icontext CLI — encrypted AI context vault for Claude Code, Codex, Cursor, and OpenCode.""" +"""fbrain CLI: encrypted AI context vault for Claude Code, Codex, Cursor, and OpenCode.""" from __future__ import annotations import argparse @@ -9,7 +9,20 @@ import sys from pathlib import Path -__version__ = "0.4.0" +__version__ = "0.5.0" + +CANONICAL_SKILLS = [ + "fbrain-populate-profile", + "fbrain-refresh-profile", + "fbrain-share-card", + "fbrain-write-fact", +] +LEGACY_SKILLS = [ + "icontext-populate-profile", + "icontext-refresh-profile", + "icontext-share-card", + "icontext-write-fact", +] # --------------------------------------------------------------------------- @@ -52,7 +65,7 @@ def _print(msg: str = "", **kwargs) -> None: def _header(cmd: str) -> None: _print("") _print(_hr()) - _print(f" {_c(C.BOLD, f'icontext · {cmd}')}") + _print(f" {_c(C.BOLD, f'fbrain · {cmd}')}") _print(_hr()) @@ -63,7 +76,7 @@ def _header(cmd: str) -> None: def _resolve_vault(vault_arg: str | None) -> Path: if vault_arg: return Path(vault_arg).expanduser().resolve() - env = os.environ.get("ICONTEXT_VAULT") + env = os.environ.get("FBRAIN_VAULT") or os.environ.get("ICONTEXT_VAULT") if env: return Path(env).expanduser().resolve() default = Path("~/context").expanduser().resolve() @@ -73,10 +86,10 @@ def _resolve_vault(vault_arg: str | None) -> Path: "\n" + _err("Vault not found.") + "\n\n" - + _info("Run 'icontext init' to create your vault, or specify the path:") + + _info("Run 'fbrain init' to create your vault, or specify the path:") + "\n" - + " icontext --vault /path/to/vault \n" - + " ICONTEXT_VAULT=/path/to/vault icontext \n" + + " fbrain --vault /path/to/vault \n" + + " FBRAIN_VAULT=/path/to/vault fbrain \n" ) @@ -148,7 +161,7 @@ def cmd_status(args: argparse.Namespace) -> int: if not vault.exists(): _print(_err(f"Vault not found: {vault}")) - _print(_info("Run 'icontext init' to create a vault, or check the path.")) + _print(_info("Run 'fbrain init' to create a vault, or check the path.")) return 1 _header("status") @@ -243,8 +256,8 @@ def cmd_sync(args: argparse.Namespace) -> int: _print(_warn("No sources configured yet.")) _print("") _print(_info("Connect a source first:")) - _print(" icontext connect gmail") - _print(" icontext connect linkedin --pdf ~/Downloads/Profile.pdf") + _print(" fbrain connect gmail") + _print(" fbrain connect linkedin --pdf ~/Downloads/Profile.pdf") _print("") return 1 @@ -274,7 +287,7 @@ def cmd_sync(args: argparse.Namespace) -> int: _print(" Open Claude Code and ask:") _print(f' {_c(C.DIM, chr(34) + "What do you know about me?" + chr(34))}') _print(_hr()) - _print(_info("run `icontext doctor` to verify Claude Code has your profile")) + _print(_info("run `fbrain doctor` to verify Claude Code has your profile")) _print("") return exit_code @@ -287,7 +300,7 @@ def cmd_search(args: argparse.Namespace) -> int: try: from indexlib import search except ImportError: - sys.exit(_err("indexlib not found. Run from icontext repo root or after install.")) + sys.exit(_err("indexlib not found. Run from fbrain repo root or after install.")) results = search(vault, args.query, limit=args.limit, tier=args.tier or None) if not results: @@ -307,7 +320,7 @@ def cmd_rebuild(args: argparse.Namespace) -> int: try: from indexlib import rebuild except ImportError: - sys.exit(_err("indexlib not found. Run from icontext repo root or after install.")) + sys.exit(_err("indexlib not found. Run from fbrain repo root or after install.")) _print(_info(f"Rebuilding index for {vault}...")) count = rebuild(vault) @@ -344,16 +357,16 @@ def cmd_init(args: argparse.Namespace) -> int: ) if not result.stdout.strip(): _sp.run( - ["git", "config", "user.email", "icontext@local"], + ["git", "config", "user.email", "fbrain@local"], cwd=str(vault), capture_output=True, ) _sp.run( - ["git", "config", "user.name", "icontext"], + ["git", "config", "user.name", "fbrain"], cwd=str(vault), capture_output=True, ) _sp.run( - ["git", "-C", str(vault), "commit", "--allow-empty", "-m", "init: icontext vault"], + ["git", "-C", str(vault), "commit", "--allow-empty", "-m", "init: fbrain vault"], check=True, capture_output=True, ) @@ -371,20 +384,20 @@ def cmd_init(args: argparse.Namespace) -> int: _print("") _print(" Next: open Claude Code in this directory and paste:") _print("") - _print(f" {_c(C.BOLD, 'Populate my icontext profile from Gmail.')}") + _print(f" {_c(C.BOLD, 'Populate my fbrain profile from Gmail.')}") _print("") _print(f" {_c(C.DIM, 'That is it. Claude will use its Gmail MCP to build your profile.')}") _print("") _print(f" {_c(C.DIM, 'or, for headless setups (requires GEMINI_API_KEY):')}") - _print(" icontext connect gmail") - _print(" icontext sync") + _print(" fbrain connect gmail") + _print(" fbrain sync") _print("") return 0 def _install_skills() -> tuple[int, list[str]]: - """Install icontext skill files into ~/.claude/skills/ and ~/.cursor/rules/. + """Install fbrain skill files into ~/.claude/skills/ and ~/.cursor/rules/. Returns (count_installed, list_of_status_messages). """ @@ -398,12 +411,7 @@ def _install_skills() -> tuple[int, list[str]]: msgs.append(_warn("skills/ source dir not found — skipping skill install")) return 0, msgs - skill_names = [ - "icontext-populate-profile", - "icontext-refresh-profile", - "icontext-share-card", - "icontext-write-fact", - ] + skill_names = CANONICAL_SKILLS + LEGACY_SKILLS claude_skills_dir = Path("~/.claude/skills").expanduser() cursor_rules_dir = Path("~/.cursor/rules").expanduser() claude_skills_dir.mkdir(parents=True, exist_ok=True) @@ -428,19 +436,19 @@ def _install_skills() -> tuple[int, list[str]]: count += 1 if count > 0: - msgs.append(_ok(f"{count} skill(s) installed (Claude Code + Cursor)")) + msgs.append(_ok(f"{count} skill file(s) installed (Claude Code + Cursor)")) return count, msgs def _install_claude_md_snippet(vault: Path) -> None: - """Write or update the icontext snippet in ~/.claude/CLAUDE.md.""" + """Write or update the fbrain snippet in ~/.claude/CLAUDE.md.""" claude_md = Path("~/.claude/CLAUDE.md").expanduser() home = str(Path("~").expanduser()) vault_short = str(vault).replace(home, "~") snippet = ( - "\n" - "## iContext (your context vault)\n\n" + "\n" + "## fbrain (your context vault)\n\n" f"Your context vault is at {vault_short} with this structure:\n\n" " internal/profile/ — private synthesized profile\n" " user.md — full profile (identity, relationships, projects)\n" @@ -451,16 +459,16 @@ def _install_claude_md_snippet(vault: Path) -> None: "ALWAYS read internal/profile/user.md at session start before answering personal\n" "or work questions about the user.\n\n" "If files are missing or older than 7 days, offer to populate/refresh.\n" - "To populate, invoke the icontext-populate-profile skill.\n\n" + "To populate, invoke the fbrain-populate-profile skill.\n\n" "Available skills:\n" - "- icontext-populate-profile (build profile from Gmail/LinkedIn/chat)\n" - "- icontext-refresh-profile (update stale profile)\n" - "- icontext-share-card (regenerate shareable summary)\n" - "- icontext-write-fact (route a fact to the correct vault location)\n\n" - "Multi-device sync: at session start, run `icontext pull` to fetch any updates\n" + "- fbrain-populate-profile (build profile from Gmail/LinkedIn/chat)\n" + "- fbrain-refresh-profile (update stale profile)\n" + "- fbrain-share-card (regenerate shareable summary)\n" + "- fbrain-write-fact (route a fact to the correct vault location)\n\n" + "Multi-device sync: at session start, run `fbrain pull` to fetch any updates\n" "from other machines. The user-prompt-submit hook does this automatically if a\n" "remote is configured.\n" - "" + "" ) if claude_md.exists(): @@ -469,7 +477,7 @@ def _install_claude_md_snippet(vault: Path) -> None: claude_md.parent.mkdir(parents=True, exist_ok=True) existing = "" - pattern = re.compile(r".*?", re.DOTALL) + pattern = re.compile(r".*?", re.DOTALL) if pattern.search(existing): new_text = pattern.sub(snippet, existing) if new_text != existing: @@ -496,8 +504,8 @@ def cmd_share(args: argparse.Namespace) -> int: _print("") _print(" The card is generated automatically during your first Gmail sync.") _print("") - _print(_info("icontext connect gmail # if not done yet")) - _print(_info("icontext sync # generates the card")) + _print(_info("fbrain connect gmail # if not done yet")) + _print(_info("fbrain sync # generates the card")) _print("") return 1 @@ -518,18 +526,13 @@ def cmd_share(args: argparse.Namespace) -> int: def cmd_skills(args: argparse.Namespace) -> int: - """List or update installed icontext skills.""" + """List or update installed fbrain skills.""" import subprocess as _sp action = getattr(args, "skills_action", None) or "list" claude_skills_dir = Path("~/.claude/skills").expanduser() cursor_rules_dir = Path("~/.cursor/rules").expanduser() - skill_names = [ - "icontext-populate-profile", - "icontext-refresh-profile", - "icontext-share-card", - "icontext-write-fact", - ] + skill_names = CANONICAL_SKILLS if action == "list": _header("skills") @@ -547,18 +550,20 @@ def cmd_skills(args: argparse.Namespace) -> int: if action == "update": _header("skills · update") _print("") - # Pull latest skill files from the icontext repo - icontext_dir = Path("~/icontext").expanduser() - if (icontext_dir / ".git").exists(): - _print(_info("pulling latest from floomhq/icontext...")) + # Pull latest skill files from the fbrain repo + fbrain_dir = Path(os.environ.get("FBRAIN_ROOT", "~/fbrain")).expanduser() + legacy_dir = Path("~/icontext").expanduser() + repo_dir = fbrain_dir if (fbrain_dir / ".git").exists() else legacy_dir + if (repo_dir / ".git").exists(): + _print(_info("pulling latest from floomhq/fbrain...")) result = _sp.run( - ["git", "-C", str(icontext_dir), "pull", "--ff-only", "--quiet"], + ["git", "-C", str(repo_dir), "pull", "--ff-only", "--quiet"], capture_output=True, text=True, ) if result.returncode != 0: _print(_warn(f"git pull failed: {result.stderr.strip()}")) else: - _print(_warn(f"no icontext repo at {icontext_dir} — using bundled skills")) + _print(_warn(f"no fbrain repo at {fbrain_dir} — using bundled skills")) count, msgs = _install_skills() for msg in msgs: @@ -595,7 +600,7 @@ def cmd_push(args: argparse.Namespace) -> int: return 1 if not (vault / ".git").exists(): _print(_err(f"Vault is not a git repo: {vault}")) - _print(_info("Run 'icontext init' first.")) + _print(_info("Run 'fbrain init' first.")) return 1 _header("push") @@ -618,7 +623,7 @@ def cmd_push(args: argparse.Namespace) -> int: if n_changed > 0: # Commit from datetime import datetime - msg = f"icontext: sync {datetime.now().strftime('%Y-%m-%d %H:%M')}" + msg = f"fbrain: sync {datetime.now().strftime('%Y-%m-%d %H:%M')}" commit = subprocess.run( ["git", "-C", str(vault), "commit", "-m", msg], capture_output=True, text=True, @@ -722,29 +727,32 @@ def cmd_pull(args: argparse.Namespace) -> int: # autosync # --------------------------------------------------------------------------- -LAUNCHD_LABEL = "dev.icontext.autosync" -SYSTEMD_SERVICE = "icontext-autosync.service" -SYSTEMD_TIMER = "icontext-autosync.timer" +LAUNCHD_LABEL = "dev.fbrain.autosync" +SYSTEMD_SERVICE = "fbrain-autosync.service" +SYSTEMD_TIMER = "fbrain-autosync.timer" def _launchd_plist_path() -> Path: - return Path("~/Library/LaunchAgents/dev.icontext.autosync.plist").expanduser() + return Path("~/Library/LaunchAgents/dev.fbrain.autosync.plist").expanduser() def _launchd_log_path() -> Path: - return Path("~/Library/Logs/icontext.log").expanduser() + return Path("~/Library/Logs/fbrain.log").expanduser() def _systemd_unit_dir() -> Path: return Path("~/.config/systemd/user").expanduser() -def _icontext_bin() -> str: - """Best-effort path to the icontext executable for use in service files.""" +def _fbrain_bin() -> str: + """Best-effort path to the fbrain executable for use in service files.""" import shutil as _sh - found = _sh.which("icontext") + found = _sh.which("fbrain") if found: return found + legacy = _sh.which("icontext") + if legacy: + return legacy # Fall back to invoking cli.py directly via the current python return f"{sys.executable} {Path(__file__).resolve()}" @@ -754,10 +762,10 @@ def _autosync_start_macos(vault: Path) -> int: plist.parent.mkdir(parents=True, exist_ok=True) log_path = _launchd_log_path() log_path.parent.mkdir(parents=True, exist_ok=True) - icontext = _icontext_bin() - # icontext might be " /cli.py"; split it. + fbrain = _fbrain_bin() + # fbrain might be " /cli.py"; split it. program_args_xml = "" - parts = icontext.split() + parts = fbrain.split() for part in parts: program_args_xml += f" {part}\n" program_args_xml += " push\n" @@ -828,22 +836,22 @@ def _systemctl_user_env() -> dict: def _autosync_start_linux(vault: Path) -> int: unit_dir = _systemd_unit_dir() unit_dir.mkdir(parents=True, exist_ok=True) - icontext = _icontext_bin() + fbrain = _fbrain_bin() service_path = unit_dir / SYSTEMD_SERVICE timer_path = unit_dir / SYSTEMD_TIMER service_path.write_text( f"""[Unit] -Description=iContext autosync (push vault to origin) +Description=fbrain autosync (push vault to origin) [Service] Type=oneshot -ExecStart={icontext} push --vault {vault} +ExecStart={fbrain} push --vault {vault} """ ) timer_path.write_text( f"""[Unit] -Description=iContext autosync timer +Description=fbrain autosync timer [Timer] OnBootSec=2min @@ -875,7 +883,7 @@ def _autosync_start_linux(vault: Path) -> int: _print(_info("Then re-run with the user bus exported:")) _print(' XDG_RUNTIME_DIR=/run/user/$(id -u) \\') _print(' DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus \\') - _print(' icontext autosync start --vault ' + str(vault)) + _print(' fbrain autosync start --vault ' + str(vault)) return 1 _print(_ok(f"timer enabled ({SYSTEMD_TIMER}; runs every 60s)")) return 0 @@ -924,7 +932,7 @@ def _autosync_status_macos() -> int: if list_out.returncode == 0: _print(_ok(f"status: running ({LAUNCHD_LABEL})")) else: - _print(_warn(f"status: plist installed but not loaded — run 'icontext autosync start'")) + _print(_warn(f"status: plist installed but not loaded — run 'fbrain autosync start'")) if log_path.exists(): mtime = log_path.stat().st_mtime from datetime import datetime @@ -1018,42 +1026,42 @@ def cmd_doctor(args: argparse.Namespace) -> int: def main() -> int: parser = argparse.ArgumentParser( - prog="icontext", + prog="fbrain", description="Encrypted AI context vault for Claude Code, Codex, Cursor, OpenCode.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "examples:\n" - " icontext init # set up vault + skills\n" - " icontext skills list # show installed skills\n" - " icontext skills update # pull latest skills\n" - " icontext status # show vault state\n" - " icontext search 'fundraising'\n" + " fbrain init # set up vault + skills\n" + " fbrain skills list # show installed skills\n" + " fbrain skills update # pull latest skills\n" + " fbrain status # show vault state\n" + " fbrain search 'fundraising'\n" "\n" "headless / fallback (requires GEMINI_API_KEY):\n" - " icontext connect gmail\n" - " icontext connect linkedin --pdf ~/Downloads/Profile.pdf\n" - " icontext sync\n" + " fbrain connect gmail\n" + " fbrain connect linkedin --pdf ~/Downloads/Profile.pdf\n" + " fbrain sync\n" "\n" - "docs: https://icontext.floom.dev\n" - "issues: https://github.com/floomhq/icontext/issues" + "docs: https://floom.dev/fbrain\n" + "issues: https://github.com/floomhq/fbrain/issues" ), ) parser.add_argument( "--vault", metavar="PATH", - help="path to vault directory (overrides ICONTEXT_VAULT env var; default: ~/context)", + help="path to vault directory (overrides FBRAIN_VAULT env var; default: ~/context)", ) parser.add_argument( - "--version", action="version", version=f"icontext {__version__}", + "--version", action="version", version=f"fbrain {__version__}", ) sub = parser.add_subparsers(dest="command", metavar="COMMAND") def _add_vault_arg(p: argparse.ArgumentParser) -> None: # default=argparse.SUPPRESS means this won't override the parent parser's --vault - # when the user puts --vault before the subcommand (e.g. icontext --vault PATH status) + # when the user puts --vault before the subcommand (e.g. fbrain --vault PATH status) p.add_argument( "--vault", metavar="PATH", default=argparse.SUPPRESS, - help="path to vault directory (overrides ICONTEXT_VAULT env var; default: ~/context)", + help="path to vault directory (overrides FBRAIN_VAULT env var; default: ~/context)", ) # init @@ -1062,13 +1070,13 @@ def _add_vault_arg(p: argparse.ArgumentParser) -> None: help="set up a new vault and install agent skills", description=( "Create a new context vault at ~/context (or --vault PATH), initialise a git\n" - "repo, install icontext skills for Claude Code and Cursor, and insert a snippet\n" + "repo, install fbrain skills for Claude Code and Cursor, and insert a snippet\n" "into ~/.claude/CLAUDE.md so your agent loads your profile at session start.\n" "\n" "After init, open Claude Code and say:\n" - " \"Populate my icontext profile\"\n" + " \"Populate my fbrain profile\"\n" "\n" - "No API keys required. Headless `icontext sync` is also available as a fallback\n" + "No API keys required. Headless `fbrain sync` is also available as a fallback\n" "(requires GEMINI_API_KEY)." ), formatter_class=argparse.RawDescriptionHelpFormatter, @@ -1102,7 +1110,7 @@ def _add_vault_arg(p: argparse.ArgumentParser) -> None: "\n" " linkedin — Extract professional profile from a LinkedIn PDF export.\n" " Download: linkedin.com/in/you → More → Save to PDF\n" - " Then: icontext connect linkedin --pdf ~/Downloads/Profile.pdf" + " Then: fbrain connect linkedin --pdf ~/Downloads/Profile.pdf" ), formatter_class=argparse.RawDescriptionHelpFormatter, ) @@ -1127,11 +1135,11 @@ def _add_vault_arg(p: argparse.ArgumentParser) -> None: "Pulls fresh data from connected sources and regenerates the AI profile\n" "using Gemini. Requires GEMINI_API_KEY.\n" "\n" - " icontext sync # sync all configured sources\n" - " icontext sync gmail # sync Gmail only\n" - " icontext sync linkedin # sync LinkedIn only\n" + " fbrain sync # sync all configured sources\n" + " fbrain sync gmail # sync Gmail only\n" + " fbrain sync linkedin # sync LinkedIn only\n" "\n" - "For most users: open Claude Code and say \"populate my icontext profile\"\n" + "For most users: open Claude Code and say \"populate my fbrain profile\"\n" "instead. The agent uses its own tools (Gmail MCP, browser, PDF) and\n" "writes the same files." ), @@ -1151,9 +1159,9 @@ def _add_vault_arg(p: argparse.ArgumentParser) -> None: description=( "Full-text search across the vault index.\n" "\n" - " icontext search 'fundraising'\n" - " icontext search 'YC' --tier shareable\n" - " icontext search 'investors' --limit 10\n" + " fbrain search 'fundraising'\n" + " fbrain search 'YC' --tier shareable\n" + " fbrain search 'investors' --limit 10\n" "\n" "Tiers: shareable (public-safe), internal (private), vault (all files)." ), @@ -1208,15 +1216,15 @@ def _add_vault_arg(p: argparse.ArgumentParser) -> None: "skills", help="list or update installed skills", description=( - "Manage the icontext skills installed for Claude Code and Cursor.\n" + "Manage the fbrain skills installed for Claude Code and Cursor.\n" "\n" - " icontext skills list # show installed skills and target tools\n" - " icontext skills update # pull latest skill versions from the icontext repo\n" + " fbrain skills list # show installed skills and target tools\n" + " fbrain skills update # pull latest skill versions from the fbrain repo\n" "\n" "Skills are Markdown instructions that your AI agent reads to populate\n" "and refresh your profile. They live at:\n" - " ~/.claude/skills/icontext-*/SKILL.md\n" - " ~/.cursor/rules/icontext-*.mdc" + " ~/.claude/skills/fbrain-*/SKILL.md\n" + " ~/.cursor/rules/fbrain-*.mdc" ), formatter_class=argparse.RawDescriptionHelpFormatter, ) @@ -1232,7 +1240,7 @@ def _add_vault_arg(p: argparse.ArgumentParser) -> None: description=( "Stage and commit any local changes in the vault, then push to origin.\n" "\n" - "Used for multi-device sync: run on device A, then 'icontext pull' on device B.\n" + "Used for multi-device sync: run on device A, then 'fbrain pull' on device B.\n" "If no origin remote is configured, prints setup instructions for\n" "'gh repo create /context --private --source=. --push'." ), @@ -1260,16 +1268,16 @@ def _add_vault_arg(p: argparse.ArgumentParser) -> None: "autosync", help="manage the background autosync agent (60s push)", description=( - "Manage a background agent that runs 'icontext push' every 60 seconds.\n" + "Manage a background agent that runs 'fbrain push' every 60 seconds.\n" "\n" - " icontext autosync start # install + start the agent\n" - " icontext autosync stop # stop and remove the agent\n" - " icontext autosync status # show running state and last sync time\n" + " fbrain autosync start # install + start the agent\n" + " fbrain autosync stop # stop and remove the agent\n" + " fbrain autosync status # show running state and last sync time\n" "\n" "Implementation: launchd on macOS (~/Library/LaunchAgents/), systemd user\n" "timer on Linux (~/.config/systemd/user/). Logs:\n" - " macOS: ~/Library/Logs/icontext.log\n" - " Linux: journalctl --user -u icontext-autosync.service" + " macOS: ~/Library/Logs/fbrain.log\n" + " Linux: journalctl --user -u fbrain-autosync.service" ), formatter_class=argparse.RawDescriptionHelpFormatter, ) @@ -1307,5 +1315,14 @@ def _add_vault_arg(p: argparse.ArgumentParser) -> None: return args.func(args) +def _deprecated_main() -> int: + print( + "warning: 'icontext' is deprecated, use 'fbrain' instead. " + "This shim will be removed in v0.6.0.", + file=sys.stderr, + ) + return main() + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/config/gitleaks.toml b/config/gitleaks.toml index e534468..db9dc4d 100644 --- a/config/gitleaks.toml +++ b/config/gitleaks.toml @@ -1,11 +1,11 @@ -# icontext gitleaks config +# fbrain gitleaks config # Extends the gitleaks default ruleset with vault-specific allowlists. [extend] useDefault = true [allowlist] -description = "icontext: paths and patterns that are OK" +description = "fbrain: paths and patterns that are OK" paths = [ '''\.git/''', diff --git a/config/tiers.yml b/config/tiers.yml index 5c015bb..736b2ad 100644 --- a/config/tiers.yml +++ b/config/tiers.yml @@ -1,4 +1,4 @@ -# icontext sensitivity tiers. +# fbrain sensitivity tiers. # # rank: higher means more sensitive. # enforce_unclassified_paths=false keeps legacy repositories usable while they diff --git a/connectors/base.py b/connectors/base.py index 96057f3..8185bbd 100644 --- a/connectors/base.py +++ b/connectors/base.py @@ -1,4 +1,4 @@ -"""Base connector interface for icontext data sources.""" +"""Base connector interface for fbrain data sources.""" from __future__ import annotations import json @@ -78,7 +78,7 @@ def write_profile(self, vault: Path, rel_path: str, content: str) -> None: out.write_text(content) def commit_profiles(self, vault: Path) -> None: - """Stage and commit all icontext changes in a single atomic commit.""" + """Stage and commit all fbrain changes in a single atomic commit.""" try: subprocess.run( ["git", "-C", str(vault), "add", "-A"], @@ -86,7 +86,7 @@ def commit_profiles(self, vault: Path) -> None: ) result = subprocess.run( ["git", "-C", str(vault), "commit", "-m", - f"icontext: sync {self.name} {datetime.now(UTC).strftime('%Y-%m-%d')}"], + f"fbrain: sync {self.name} {datetime.now(UTC).strftime('%Y-%m-%d')}"], capture_output=True, text=True, ) # Exit 1 with "nothing to commit" is acceptable; surface other errors. @@ -111,7 +111,7 @@ def _gemini_configure(self): " 2. Export it in your current shell:\n" " export GEMINI_API_KEY=your_key_here\n" " 3. Make it permanent — add that line to ~/.zshrc or ~/.bashrc,\n" - " then open a new terminal and re-run: icontext sync" + " then open a new terminal and re-run: fbrain sync" ) try: import google.generativeai as genai @@ -119,7 +119,7 @@ def _gemini_configure(self): raise RuntimeError( "google-generativeai is not installed.\n" " Run: pip install google-generativeai\n" - " Then re-run: icontext sync" + " Then re-run: fbrain sync" ) genai.configure(api_key=api_key) return genai @@ -128,7 +128,7 @@ def gemini_synthesize(self, prompt: str) -> str: """Free-form Gemini call. Used for shareable card and legacy paths.""" genai = self._gemini_configure() print(" synthesizing with Gemini...", end="", flush=True) - model_name = os.environ.get("ICONTEXT_GEMINI_MODEL", "gemini-2.5-flash-lite") + model_name = os.environ.get("FBRAIN_GEMINI_MODEL") or os.environ.get("ICONTEXT_GEMINI_MODEL", "gemini-2.5-flash-lite") model = genai.GenerativeModel(model_name) response = model.generate_content(prompt) print(" ✓") @@ -142,7 +142,7 @@ def gemini_synthesize(self, prompt: str) -> str: def gemini_json(self, prompt: str, schema: dict) -> dict: """JSON-mode Gemini call with a typed schema. Returns parsed dict.""" genai = self._gemini_configure() - model_name = os.environ.get("ICONTEXT_GEMINI_MODEL", "gemini-2.5-flash-lite") + model_name = os.environ.get("FBRAIN_GEMINI_MODEL") or os.environ.get("ICONTEXT_GEMINI_MODEL", "gemini-2.5-flash-lite") model = genai.GenerativeModel(model_name) response = model.generate_content( prompt, diff --git a/connectors/gmail.py b/connectors/gmail.py index 8109a09..18ec11e 100644 --- a/connectors/gmail.py +++ b/connectors/gmail.py @@ -1,4 +1,4 @@ -"""Gmail IMAP connector for icontext.""" +"""Gmail IMAP connector for fbrain.""" from __future__ import annotations import getpass @@ -804,7 +804,7 @@ def _render_profile_md(profile: dict, sources: list[str], scan_days: int, f"source: {', '.join(sources)} (last {scan_days} days)" if scan_days else f"source: {', '.join(sources)}", f"accounts: {', '.join(accounts)}" if accounts else "", f"generated: {today}", - "refresh: icontext sync gmail", + "refresh: fbrain sync gmail", "---", "", "## Identity Summary", @@ -873,7 +873,7 @@ def _render_profile_md(profile: dict, sources: list[str], scan_days: int, def _render_relationships_md(profile: dict, today: str) -> str: rels = profile.get("key_relationships") or [] - body = ["---", "source: icontext/gmail", f"generated: {today}", "---", "", + body = ["---", "source: fbrain/gmail", f"generated: {today}", "---", "", "## Key Relationships", ""] if rels: body.append("| Name | Company | Role | Frequency | Warmth | Context |") @@ -891,7 +891,7 @@ def _render_relationships_md(profile: dict, today: str) -> str: def _render_projects_md(profile: dict, today: str) -> str: projects = profile.get("active_projects") or [] - body = ["---", "source: icontext/gmail", f"generated: {today}", "---", "", + body = ["---", "source: fbrain/gmail", f"generated: {today}", "---", "", "## Active Projects", ""] if projects: for p in projects: @@ -916,7 +916,7 @@ def _render_card_md(profile: dict, today: str) -> str: "---\n" "shareable: true\n" f"generated: {today}\n" - "source: icontext\n" + "source: fbrain\n" "---\n\n" f"{card or '_(no card available)_'}\n" ) @@ -985,7 +985,7 @@ class GmailConnector(BaseConnector): def connect(self, vault: Path) -> None: _print("") _print(_hr()) - _print(f" {_c(C.BOLD, 'icontext · connect gmail')}") + _print(f" {_c(C.BOLD, 'fbrain · connect gmail')}") _print(_hr()) _print("") _print(f" Gmail requires an App Password.") @@ -995,7 +995,7 @@ def connect(self, vault: Path) -> None: _print("") _print(f" {_c(C.BOLD, 'Step 2')} — Create App Password:") _print(_info("https://myaccount.google.com/apppasswords")) - _print(_info("App name: icontext → click Create → copy 16-char code")) + _print(_info("App name: fbrain → click Create → copy 16-char code")) _print("") _print(_warn("Work/school accounts may have App Passwords disabled.")) _print(f" Use a personal Gmail if so.") @@ -1086,7 +1086,7 @@ def sync(self, vault: Path) -> str: if not accounts: raise RuntimeError( "No Gmail accounts configured.\n" - " Run: icontext connect gmail" + " Run: fbrain connect gmail" ) scan_days = int(cfg.get("scan_days", 90)) @@ -1103,7 +1103,7 @@ def sync(self, vault: Path) -> str: if not pwd: pwd = acct.get("app_password", "") if not pwd: - _print(_warn(f"no password for {addr} — run: icontext connect gmail")) + _print(_warn(f"no password for {addr} — run: fbrain connect gmail")) continue own_addresses.add(addr.lower()) @@ -1122,7 +1122,7 @@ def sync(self, vault: Path) -> str: if sys.stdout.isatty(): print(f" {_c(C.RED, '✗')}") _print(_err(f"Login failed for {addr}: {exc}")) - _print(_warn(f"Re-run 'icontext connect gmail' to update the app password.")) + _print(_warn(f"Re-run 'fbrain connect gmail' to update the app password.")) continue except OSError as exc: if sys.stdout.isatty(): @@ -1159,7 +1159,7 @@ def sync(self, vault: Path) -> str: "No messages retrieved from any Gmail account.\n" " Possible causes:\n" " - IMAP is disabled in Gmail settings → enable at gmail.com/settings → Forwarding and POP/IMAP\n" - " - App password is stale — re-run: icontext connect gmail\n" + " - App password is stale — re-run: fbrain connect gmail\n" f" - No messages in the last {scan_days} days (scan window)" ) diff --git a/connectors/linkedin.py b/connectors/linkedin.py index 3b99b06..5dabe05 100644 --- a/connectors/linkedin.py +++ b/connectors/linkedin.py @@ -1,4 +1,4 @@ -"""LinkedIn PDF connector for icontext.""" +"""LinkedIn PDF connector for fbrain.""" from __future__ import annotations import json @@ -90,7 +90,7 @@ def _read_pdf_text(pdf_path: Path) -> str: f"PDF appears to be empty or image-only: {pdf_path.name}\n" " LinkedIn PDFs saved via browser Print→Save are image-only and cannot be parsed.\n" " Use the official export instead: linkedin.com/in/you → More → Save to PDF\n" - " Then re-run: icontext connect linkedin --pdf ~/Downloads/Profile.pdf" + " Then re-run: fbrain connect linkedin --pdf ~/Downloads/Profile.pdf" ) except ImportError: raise RuntimeError( @@ -100,7 +100,7 @@ def _read_pdf_text(pdf_path: Path) -> str: " brew install poppler # installs pdftotext (recommended)\n" " pip install pypdf # pure-Python fallback\n" "\n" - " Then re-run: icontext connect linkedin --pdf {pdf_path}" + " Then re-run: fbrain connect linkedin --pdf {pdf_path}" ) @@ -110,7 +110,7 @@ def _render_linkedin_md(profile: dict, today: str, pdf_name: str) -> str: "source: LinkedIn PDF", f"pdf_file: {pdf_name}", f"generated: {today}", - "refresh: icontext sync linkedin", + "refresh: fbrain sync linkedin", "---", "", "## Professional Summary", @@ -170,7 +170,7 @@ class LinkedInConnector(BaseConnector): def connect(self, vault: Path, pdf_path: str | None = None) -> None: _print("") _print(_hr()) - _print(f" {_c(C.BOLD, 'icontext · connect linkedin')}") + _print(f" {_c(C.BOLD, 'fbrain · connect linkedin')}") _print(_hr()) if pdf_path is None: @@ -202,13 +202,13 @@ def connect(self, vault: Path, pdf_path: str | None = None) -> None: for p in pdf_candidates[-5:]: _print(_info(p.name)) _print("") - _print(_info(f"Re-run: icontext connect linkedin --pdf ~/Downloads/{pdf_candidates[-1].name}")) + _print(_info(f"Re-run: fbrain connect linkedin --pdf ~/Downloads/{pdf_candidates[-1].name}")) else: _print("") _print(_warn("No PDFs found in ~/Downloads. Download your LinkedIn PDF first:")) _print(_info("linkedin.com/in/your-username → More → Save to PDF")) _print("") - _print(_info("Then re-run: icontext connect linkedin")) + _print(_info("Then re-run: fbrain connect linkedin")) return if export_path.suffix.lower() != ".pdf": @@ -224,7 +224,7 @@ def connect(self, vault: Path, pdf_path: str | None = None) -> None: f"{export_path.name} is too short to be a valid LinkedIn export " f"({len(text.strip())} chars — expected at least 100).\n" " Make sure you used LinkedIn → More → Save to PDF, not a browser print.\n" - f" Then re-run: icontext connect linkedin --pdf {export_path}" + f" Then re-run: fbrain connect linkedin --pdf {export_path}" ) _print(f" {C.GREEN}✓{C.RESET} ({len(text)} chars)") except RuntimeError: @@ -236,19 +236,19 @@ def connect(self, vault: Path, pdf_path: str | None = None) -> None: _print(_ok("LinkedIn connected")) _print(_hr()) - _print(_info("Run: icontext sync")) + _print(_info("Run: fbrain sync")) def sync(self, vault: Path) -> str: cfg = self.load_config(vault) pdf_path_str = cfg.get("pdf_path") if not pdf_path_str: - raise RuntimeError("No LinkedIn PDF configured. Run: icontext connect linkedin") + raise RuntimeError("No LinkedIn PDF configured. Run: fbrain connect linkedin") pdf_path = Path(pdf_path_str) if not pdf_path.exists(): raise RuntimeError( f"LinkedIn PDF no longer exists at: {pdf_path}\n" - " Re-run: icontext connect linkedin --pdf /path/to/Profile.pdf" + " Re-run: fbrain connect linkedin --pdf /path/to/Profile.pdf" ) label_width = 36 @@ -269,7 +269,7 @@ def sync(self, vault: Path) -> str: f"No text could be extracted from {pdf_path.name}.\n" " The file may be image-only (browser-printed PDF).\n" " Use the official export: linkedin.com/in/you → More → Save to PDF\n" - f" Then re-run: icontext connect linkedin --pdf {pdf_path}" + f" Then re-run: fbrain connect linkedin --pdf {pdf_path}" ) # Soft cap; the JSON-schema call handles structure, so we don't need to @@ -288,7 +288,7 @@ def sync(self, vault: Path) -> str: if missing: raise RuntimeError( f"LinkedIn synthesis missing required fields: {', '.join(missing)}.\n" - " Re-run: icontext sync linkedin" + " Re-run: fbrain sync linkedin" ) write_label = "writing profile..." diff --git a/demo/README-demo.md b/demo/README-demo.md index 1ebfabf..16df0ab 100644 --- a/demo/README-demo.md +++ b/demo/README-demo.md @@ -1,4 +1,4 @@ -# Recording the icontext demo +# Recording the fbrain demo ## Requirements @@ -28,5 +28,5 @@ agg demo/icontext-demo.cast demo/icontext-demo.gif --font-size 14 --theme monoka ## Embed in README ```markdown -![icontext demo](demo/icontext-demo.gif) +![fbrain demo](demo/icontext-demo.gif) ``` diff --git a/demo/demo.sh b/demo/demo.sh index f794211..fa14482 100644 --- a/demo/demo.sh +++ b/demo/demo.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# icontext demo script — rehearse before recording +# fbrain demo script — rehearse before recording # Usage: bash demo/demo.sh # Record with: asciinema rec demo/icontext-demo.cast --command "bash demo/demo.sh" @@ -14,10 +14,10 @@ pause() { sleep "${1:-0.8}"; } clear -# === icontext init === +# === fbrain init === echo "" hr -echo -e " ${BOLD}icontext · init${RESET}" +echo -e " ${BOLD}fbrain · init${RESET}" hr echo "" pause 0.4 @@ -34,7 +34,7 @@ hr echo "" echo " Next:" echo -e " ${BOLD}open Claude Code${RESET} and ask:" -echo -e " ${DIM}\"Populate my icontext profile\"${RESET}" +echo -e " ${DIM}\"Populate my fbrain profile\"${RESET}" echo "" pause 1.6 @@ -43,10 +43,10 @@ echo "" echo -e " ${DIM}# Claude Code session${RESET}" echo "" pause 0.5 -echo -e " ${CYAN}You:${RESET} populate my icontext profile" +echo -e " ${CYAN}You:${RESET} populate my fbrain profile" echo "" pause 1.2 -echo -e " ${GREEN}Claude:${RESET} I'll use the icontext-populate-profile skill." +echo -e " ${GREEN}Claude:${RESET} I'll use the fbrain-populate-profile skill." pause 0.5 echo "" printf " fetching last 90 days of Gmail metadata via MCP..."; pause 1.2; echo -e " ${GREEN}✓${RESET}" @@ -84,5 +84,5 @@ echo "" pause 1.0 hr echo "" -echo -e " ${DIM}# icontext — github.com/floomhq/icontext${RESET}" +echo -e " ${DIM}# fbrain — github.com/floomhq/fbrain${RESET}" echo "" diff --git a/docs/competitive-benchmark-2026-04-26.md b/docs/competitive-benchmark-2026-04-26.md index 5f899a1..ae752c8 100644 --- a/docs/competitive-benchmark-2026-04-26.md +++ b/docs/competitive-benchmark-2026-04-26.md @@ -1,10 +1,10 @@ -# icontext competitive benchmark +# fbrain competitive benchmark Date: 2026-04-26 ## Position -icontext is Federico's private context substrate: encrypted Git-backed tiers, +fbrain is Federico's private context substrate: encrypted Git-backed tiers, local retrieval, local agent wiring, and a doctor command that proves the setup still works. @@ -15,7 +15,7 @@ platform, Obsidian plugin, or vector database product. | Project | Verified public positioning | What to learn | What not to copy | |---|---|---|---| -| `garrytan/gstack` | Claude Code workflow stack with specialist commands for planning, review, QA, release, browser, retro, and memory. | Opinionated workflows and explicit quality gates beat generic prompts. | Do not import a large slash-command factory into icontext; Federico already has skills and workflows. | +| `garrytan/gstack` | Claude Code workflow stack with specialist commands for planning, review, QA, release, browser, retro, and memory. | Opinionated workflows and explicit quality gates beat generic prompts. | Do not import a large slash-command factory into fbrain; Federico already has skills and workflows. | | `bitbonsai/mcpvault` | Universal MCP bridge for Obsidian vaults with many note operations and client config examples. | Broad MCP client compatibility and safe file access matter. | Do not optimize for Obsidian frontmatter, tags, or plugin UX unless Federico moves this vault to Obsidian. | | `zilliztech/claude-context` | Semantic code search MCP for large codebases, backed by external vector infrastructure. | Retrieval quality matters for big corpora. | Do not require hosted vector DBs or API keys for a personal private vault baseline. | | `mksglu/context-mode` | Context window optimization and session continuity with SQLite/FTS style event tracking. | Local SQLite and compaction-aware retrieval are the right direction. | Do not sandbox every tool call or add a session recorder until Federico has a repeated failure case. | @@ -35,5 +35,5 @@ platform, Obsidian plugin, or vector database product. system is healthy. - Add embeddings only after FTS retrieval misses are observed in real sessions. - Add more write tools only after specific repeated workflows require them. -- Add gstack-style workflow commands outside icontext, not inside the vault +- Add gstack-style workflow commands outside fbrain, not inside the vault substrate. diff --git a/docs/launch-positioning.md b/docs/launch-positioning.md index 0081d95..974cc1f 100644 --- a/docs/launch-positioning.md +++ b/docs/launch-positioning.md @@ -2,12 +2,12 @@ ## One-liner -`icontext` is an encrypted AI context vault for Claude Code, Codex, Cursor, and +`fbrain` is an encrypted AI context vault for Claude Code, Codex, Cursor, and OpenCode. ## Short Description -Most AI coding agents can read a repo. `icontext` makes a private context repo +Most AI coding agents can read a repo. `fbrain` makes a private context repo safe and useful: Git-backed tiers, git-crypt encryption, gitleaks, local SQLite retrieval, MCP tools, Claude prompt injection, multi-agent config, and a doctor command that verifies the whole setup. @@ -19,7 +19,7 @@ context. Private strategy, legal files, credentials, notes, PDFs, and operating memory are usually either inaccessible to agents or pasted into prompts without controls. -`icontext` turns that messy personal context into an operational substrate: +`fbrain` turns that messy personal context into an operational substrate: encrypted at rest in Git, guarded before commit and push, searchable locally, and available to agents through explicit tools. @@ -39,13 +39,13 @@ and available to agents through explicit tools. ## Launch Post Draft -I built `icontext`: an encrypted AI context vault for Claude Code, Codex, +I built `fbrain`: an encrypted AI context vault for Claude Code, Codex, Cursor, and OpenCode. The idea is simple: your private context repo is too valuable to stay as loose Markdown and too sensitive to paste into prompts. -`icontext` gives it structure: +`fbrain` gives it structure: - `shareable/`, `internal/`, and encrypted `vault/` tiers - git-crypt encryption for sensitive history @@ -59,4 +59,4 @@ Markdown and too sensitive to paste into prompts. This changed how I work. My agents now have access to the context they need without turning my private operating system into prompt soup. -Repo: https://github.com/floomhq/icontext +Repo: https://github.com/floomhq/fbrain diff --git a/get.sh b/get.sh index 1ea4ede..0b7b667 100755 --- a/get.sh +++ b/get.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash -# icontext installer -# Usage: curl -fsSL https://raw.githubusercontent.com/floomhq/icontext/main/get.sh | bash +# fbrain installer +# Usage: curl -fsSL https://raw.githubusercontent.com/floomhq/fbrain/main/get.sh | bash set -euo pipefail -ICONTEXT_DIR="${ICONTEXT_DIR:-$HOME/icontext}" +FBRAIN_DIR="${FBRAIN_DIR:-${ICONTEXT_DIR:-$HOME/fbrain}}" BIN_DIR="$HOME/.local/bin" -echo "icontext: installing..." +echo "fbrain: installing..." # --- Dependency checks ------------------------------------------------------- MISSING_DEPS=() @@ -22,7 +22,7 @@ fi if [ "${#MISSING_DEPS[@]}" -gt 0 ]; then echo "" - echo "icontext: missing required tools: ${MISSING_DEPS[*]}" + echo "fbrain: missing required tools: ${MISSING_DEPS[*]}" echo "" if [[ "$(uname)" == "Darwin" ]]; then echo "On Mac, install them with:" @@ -37,31 +37,32 @@ if [ "${#MISSING_DEPS[@]}" -gt 0 ]; then echo " sudo apt install git python3" fi echo "" - echo "Then re-run: curl -fsSL https://icontext.floom.dev/install | bash" + echo "Then re-run: curl -fsSL https://raw.githubusercontent.com/floomhq/fbrain/main/get.sh | bash" exit 1 fi # ----------------------------------------------------------------------------- # Clone or update -if [ -d "$ICONTEXT_DIR/.git" ]; then - echo "icontext: updating $ICONTEXT_DIR" - git -C "$ICONTEXT_DIR" pull --ff-only --quiet +if [ -d "$FBRAIN_DIR/.git" ]; then + echo "fbrain: updating $FBRAIN_DIR" + git -C "$FBRAIN_DIR" pull --ff-only --quiet else - echo "icontext: cloning to $ICONTEXT_DIR" - git clone --quiet https://github.com/floomhq/icontext "$ICONTEXT_DIR" + echo "fbrain: cloning to $FBRAIN_DIR" + git clone --quiet https://github.com/floomhq/fbrain "$FBRAIN_DIR" fi # Install CLI -# Note: in agents mode, install.sh creates a symlink from ~/.local/bin/icontext -# into the vault AFTER the vault is created by `icontext init`. We install a +# Note: in agents mode, install.sh creates a symlink from ~/.local/bin/fbrain +# into the vault AFTER the vault is created by `fbrain init`. We install a # direct symlink to the repo here so the CLI is available immediately. mkdir -p "$BIN_DIR" -if command -v pip3 &>/dev/null && pip3 install -e "$ICONTEXT_DIR" --quiet 2>/dev/null; then - echo "icontext: CLI installed via pip" +if command -v pip3 &>/dev/null && pip3 install -e "$FBRAIN_DIR" --quiet 2>/dev/null; then + echo "fbrain: CLI installed via pip" else - ln -sf "$ICONTEXT_DIR/cli.py" "$BIN_DIR/icontext" - chmod +x "$ICONTEXT_DIR/cli.py" - echo "icontext: CLI linked to $BIN_DIR/icontext" + ln -sf "$FBRAIN_DIR/cli.py" "$BIN_DIR/fbrain" + ln -sf "$FBRAIN_DIR/cli.py" "$BIN_DIR/icontext" + chmod +x "$FBRAIN_DIR/cli.py" + echo "fbrain: CLI linked to $BIN_DIR/fbrain" fi # Auto-add ~/.local/bin to PATH in shell profile @@ -77,18 +78,18 @@ fi if [ -n "$SHELL_RC" ]; then if ! grep -q 'local/bin' "$SHELL_RC" 2>/dev/null; then echo '' >> "$SHELL_RC" - echo '# icontext' >> "$SHELL_RC" + echo '# fbrain' >> "$SHELL_RC" echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$SHELL_RC" - echo "icontext: added ~/.local/bin to PATH in $SHELL_RC" + echo "fbrain: added ~/.local/bin to PATH in $SHELL_RC" fi fi echo "" -echo "icontext: done." +echo "fbrain: done." echo "" echo " Next:" echo " 1. Restart your terminal (or: source ~/.zshrc)" -echo " 2. Run: icontext init" -echo " 3. Open Claude Code and say: \"populate my icontext profile\"" +echo " 2. Run: fbrain init" +echo " 3. Open Claude Code and say: \"populate my fbrain profile\"" echo "" echo " No API keys needed. Your agent does the synthesis." diff --git a/hooks/post-commit b/hooks/post-commit index 110bbc1..14bfa76 100755 --- a/hooks/post-commit +++ b/hooks/post-commit @@ -1,17 +1,17 @@ #!/usr/bin/env bash -# icontext post-commit hook: preserve Git LFS behavior and run optional indexer. +# fbrain post-commit hook: preserve Git LFS behavior and run optional indexer. set -euo pipefail -ICONTEXT_ROOT="${ICONTEXT_ROOT:-$HOME/icontext}" +FBRAIN_ROOT="${FBRAIN_ROOT:-${ICONTEXT_ROOT:-$HOME/fbrain}}" if command -v git-lfs >/dev/null 2>&1; then git lfs post-commit "$@" fi -if [ -x "$ICONTEXT_ROOT/scripts/update_index.py" ]; then - "$ICONTEXT_ROOT/scripts/update_index.py" --repo "$PWD" || { - echo "icontext: semantic index update failed; commit kept" >&2 +if [ -x "$FBRAIN_ROOT/scripts/update_index.py" ]; then + "$FBRAIN_ROOT/scripts/update_index.py" --repo "$PWD" || { + echo "fbrain: semantic index update failed; commit kept" >&2 exit 0 } fi diff --git a/hooks/pre-commit b/hooks/pre-commit index 80bb7ff..fb5208b 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -1,31 +1,31 @@ #!/usr/bin/env bash -# icontext pre-commit hook: gitleaks secret scanner +# fbrain pre-commit hook: gitleaks secret scanner # # Scans staged changes for secrets. Blocks commit on finding. -# Config: /.gitleaks.toml, falling back to icontext/config/gitleaks.toml. +# Config: /.gitleaks.toml, falling back to fbrain/config/gitleaks.toml. set -euo pipefail -ICONTEXT_ROOT="${ICONTEXT_ROOT:-$HOME/icontext}" -CONFIG="${ICONTEXT_CONFIG:-$PWD/.gitleaks.toml}" +FBRAIN_ROOT="${FBRAIN_ROOT:-${ICONTEXT_ROOT:-$HOME/fbrain}}" +CONFIG="${FBRAIN_CONFIG:-${ICONTEXT_CONFIG:-$PWD/.gitleaks.toml}}" if [ ! -f "$CONFIG" ]; then - CONFIG="$ICONTEXT_ROOT/config/gitleaks.toml" + CONFIG="$FBRAIN_ROOT/config/gitleaks.toml" fi if ! command -v gitleaks >/dev/null 2>&1; then - echo "icontext: gitleaks not installed. Run: brew install gitleaks" + echo "fbrain: gitleaks not installed. Run: brew install gitleaks" exit 1 fi if [ ! -f "$CONFIG" ]; then - echo "icontext: config not found at $CONFIG" + echo "fbrain: config not found at $CONFIG" exit 1 fi if ! gitleaks protect --staged --redact --config="$CONFIG" --no-banner; then echo "" - echo "icontext: secrets detected in staged changes. Commit blocked." + echo "fbrain: secrets detected in staged changes. Commit blocked." echo "Options:" echo " 1. Remove the secret, stage the cleaned file, recommit" echo " 2. If false positive: add pattern to $CONFIG allowlist" diff --git a/hooks/pre-push b/hooks/pre-push index 44e873b..1108443 100755 --- a/hooks/pre-push +++ b/hooks/pre-push @@ -1,10 +1,10 @@ #!/usr/bin/env bash -# icontext pre-push hook: preserve Git LFS behavior, then enforce tier placement. +# fbrain pre-push hook: preserve Git LFS behavior, then enforce tier placement. set -euo pipefail -ICONTEXT_ROOT="${ICONTEXT_ROOT:-$HOME/icontext}" -CONFIG="${ICONTEXT_TIERS_CONFIG:-.icontext-tiers.yml}" +FBRAIN_ROOT="${FBRAIN_ROOT:-${ICONTEXT_ROOT:-$HOME/fbrain}}" +CONFIG="${FBRAIN_TIERS_CONFIG:-${ICONTEXT_TIERS_CONFIG:-.icontext-tiers.yml}}" REMOTE_NAME="${1:-}" REMOTE_URL="${2:-}" @@ -19,7 +19,7 @@ if command -v git-lfs >/dev/null 2>&1; then fi if [ ! -f "$CONFIG" ]; then - echo "icontext: $CONFIG not found; skipping sensitivity tier check" + echo "fbrain: $CONFIG not found; skipping sensitivity tier check" exit 0 fi @@ -29,4 +29,4 @@ while read -r local_ref local_sha remote_ref remote_sha; do args+=(--ref-pair "$local_sha..$remote_sha") done <<<"$STDIN_PAYLOAD" -PYTHONDONTWRITEBYTECODE=1 python3 "$ICONTEXT_ROOT/scripts/check_tiers.py" --repo "$PWD" --config "$CONFIG" "${args[@]}" +PYTHONDONTWRITEBYTECODE=1 python3 "$FBRAIN_ROOT/scripts/check_tiers.py" --repo "$PWD" --config "$CONFIG" "${args[@]}" diff --git a/hooks/user-prompt-submit b/hooks/user-prompt-submit index f194b53..7358156 100755 --- a/hooks/user-prompt-submit +++ b/hooks/user-prompt-submit @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Claude UserPromptSubmit hook: inject relevant icontext search hits, +# Claude UserPromptSubmit hook: inject relevant fbrain search hits, # and trigger a background sync if the profile is stale (>7 days old). from __future__ import annotations @@ -11,21 +11,21 @@ import sys from datetime import datetime, timedelta from pathlib import Path -ICONTEXT_ROOT = Path(os.environ.get("ICONTEXT_ROOT", Path.home() / "icontext")) -ICONTEXT_VAULT = Path(os.environ.get("ICONTEXT_VAULT", Path.home() / "context")) +FBRAIN_ROOT = Path(os.environ.get("FBRAIN_ROOT") or os.environ.get("ICONTEXT_ROOT", Path.home() / "fbrain")) +FBRAIN_VAULT = Path(os.environ.get("FBRAIN_VAULT") or os.environ.get("ICONTEXT_VAULT", Path.home() / "context")) -repo = ICONTEXT_VAULT +repo = FBRAIN_VAULT # --------------------------------------------------------------------------- # Staleness check: if profile is >7 days old, trigger background sync. -# Only fires if: vault exists, icontext CLI is on PATH, and at least one +# Only fires if: vault exists, fbrain CLI is on PATH, and at least one # connector is configured (avoids noise on a fresh install). # --------------------------------------------------------------------------- profile_path = repo / "internal" / "profile" / "user.md" -icontext_bin = shutil.which("icontext") +fbrain_bin = shutil.which("fbrain") or shutil.which("icontext") connectors_cfg = repo / ".icontext" / "connectors.json" -if profile_path.exists() and icontext_bin and connectors_cfg.exists(): +if profile_path.exists() and fbrain_bin and connectors_cfg.exists(): mtime = datetime.fromtimestamp(profile_path.stat().st_mtime) if datetime.now() - mtime > timedelta(days=7): import json as _json @@ -39,7 +39,7 @@ if profile_path.exists() and icontext_bin and connectors_cfg.exists(): if configured_sources: # Fire and forget — sync all configured sources (no explicit source = syncs all) subprocess.Popen( - [icontext_bin, "sync", "--vault", str(repo)], + [fbrain_bin, "sync", "--vault", str(repo)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, @@ -49,14 +49,14 @@ if profile_path.exists() and icontext_bin and connectors_cfg.exists(): # Multi-device sync: if vault has an origin remote, pull in the background # so the agent sees fresh content from other machines. Non-blocking. # --------------------------------------------------------------------------- -if icontext_bin and (repo / ".git").exists(): +if fbrain_bin and (repo / ".git").exists(): has_origin = subprocess.run( ["git", "-C", str(repo), "remote", "get-url", "origin"], capture_output=True, text=True, ) if has_origin.returncode == 0 and has_origin.stdout.strip(): subprocess.Popen( - [icontext_bin, "pull", "--vault", str(repo)], + [fbrain_bin, "pull", "--vault", str(repo)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, @@ -65,9 +65,9 @@ if icontext_bin and (repo / ".git").exists(): # --------------------------------------------------------------------------- # Context injection: delegate to prompt_context.py # --------------------------------------------------------------------------- -prompt_context = ICONTEXT_ROOT / "scripts" / "prompt_context.py" +prompt_context = FBRAIN_ROOT / "scripts" / "prompt_context.py" if prompt_context.exists(): - os.environ["ICONTEXT_VAULT"] = str(ICONTEXT_VAULT) + os.environ["FBRAIN_VAULT"] = str(FBRAIN_VAULT) result = subprocess.run( [sys.executable, str(prompt_context)], env=os.environ, diff --git a/install.sh b/install.sh index 0fce5f5..300604d 100755 --- a/install.sh +++ b/install.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash -# icontext installer: wires hooks, configs, workflows, and local runtime into a vault repo. +# fbrain installer: wires hooks, configs, workflows, and local runtime into a vault repo. set -euo pipefail -ICONTEXT_ROOT="${ICONTEXT_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +FBRAIN_ROOT="${FBRAIN_ROOT:-${ICONTEXT_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}}" VAULT="${VAULT:-$PWD}" MODE="standard" DRY_RUN=0 @@ -34,12 +34,12 @@ while [ "$#" -gt 0 ]; do case "$1" in --vault) VAULT="${2:-}" - [ -n "$VAULT" ] || { echo "icontext: --vault requires a path"; exit 1; } + [ -n "$VAULT" ] || { echo "fbrain: --vault requires a path"; exit 1; } shift 2 ;; --mode) MODE="${2:-}" - [ -n "$MODE" ] || { echo "icontext: --mode requires minimal, standard, or agents"; exit 1; } + [ -n "$MODE" ] || { echo "fbrain: --mode requires minimal, standard, or agents"; exit 1; } shift 2 ;; --dry-run) @@ -55,7 +55,7 @@ while [ "$#" -gt 0 ]; do exit 0 ;; *) - echo "icontext: unknown option: $1" + echo "fbrain: unknown option: $1" usage exit 1 ;; @@ -65,7 +65,7 @@ done case "$MODE" in minimal|standard|agents) ;; *) - echo "icontext: invalid mode '$MODE' (expected minimal, standard, or agents)" + echo "fbrain: invalid mode '$MODE' (expected minimal, standard, or agents)" exit 1 ;; esac @@ -73,7 +73,7 @@ esac VAULT="$(cd "$VAULT" && pwd)" if [ ! -d "$VAULT/.git" ]; then - echo "icontext: $VAULT is not a git repo. cd into the vault first." + echo "fbrain: $VAULT is not a git repo. cd into the vault first." exit 1 fi @@ -124,12 +124,12 @@ write_symlink() { write_marker() { local dst="$VAULT/.icontext-installed" local source_commit="" - source_commit="$(git -C "$ICONTEXT_ROOT" rev-parse HEAD 2>/dev/null || true)" + source_commit="$(git -C "$FBRAIN_ROOT" rev-parse HEAD 2>/dev/null || true)" plan "+ .icontext-installed marker" add_manifest_entry "$dst" "" "generated" if [ "$DO_WRITE" -eq 1 ]; then cat > "$dst" <> "$temp" done - python3 - "$temp" "$manifest" "$ICONTEXT_ROOT" "$VAULT" "$MODE" <<'PY' + python3 - "$temp" "$manifest" "$FBRAIN_ROOT" "$VAULT" "$MODE" <<'PY' import hashlib import json import subprocess @@ -154,7 +154,7 @@ from pathlib import Path lines = Path(sys.argv[1]) manifest = Path(sys.argv[2]) -icontext_root = Path(sys.argv[3]) +fbrain_root = Path(sys.argv[3]) vault = Path(sys.argv[4]) mode = sys.argv[5] @@ -189,7 +189,7 @@ for raw in lines.read_text(encoding="utf-8").splitlines(): except ValueError: relative_path = str(path) try: - source_relative_path = str(source.relative_to(icontext_root)) if source else None + source_relative_path = str(source.relative_to(fbrain_root)) if source else None except ValueError: source_relative_path = None entry = { @@ -204,9 +204,9 @@ for raw in lines.read_text(encoding="utf-8").splitlines(): payload = { "schema": 1, - "tool": "icontext", + "tool": "fbrain", "mode": mode, - "icontext_commit": git_commit(icontext_root), + "fbrain_commit": git_commit(fbrain_root), "installed_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "files": entries, } @@ -217,9 +217,9 @@ PY plan "+ .icontext/manifest.json" } -echo "icontext: install plan" +echo "fbrain: install plan" echo " target: $VAULT" -echo " source: $ICONTEXT_ROOT" +echo " source: $FBRAIN_ROOT" echo " mode: $MODE" if [ "$DRY_RUN" -eq 1 ]; then echo " dry-run: yes" @@ -229,24 +229,24 @@ echo "" echo "What this installer changes:" install_actions() { - copy_file "$ICONTEXT_ROOT/config/gitleaks.toml" "$VAULT/.gitleaks.toml" ".gitleaks.toml" - copy_file "$ICONTEXT_ROOT/config/tiers.yml" "$VAULT/.icontext-tiers.yml" ".icontext-tiers.yml" + copy_file "$FBRAIN_ROOT/config/gitleaks.toml" "$VAULT/.gitleaks.toml" ".gitleaks.toml" + copy_file "$FBRAIN_ROOT/config/tiers.yml" "$VAULT/.icontext-tiers.yml" ".icontext-tiers.yml" for script in icontext_classify.py check_tiers.py indexlib.py update_index.py prompt_context.py install_claude_integration.py doctor.py eval_retrieval.py; do - copy_file "$ICONTEXT_ROOT/scripts/$script" "$VAULT/.icontext/scripts/$script" ".icontext/scripts/$script" 1 + copy_file "$FBRAIN_ROOT/scripts/$script" "$VAULT/.icontext/scripts/$script" ".icontext/scripts/$script" 1 done - copy_file "$ICONTEXT_ROOT/mcp/server.py" "$VAULT/.icontext/mcp/server.py" ".icontext/mcp/server.py" 1 + copy_file "$FBRAIN_ROOT/mcp/server.py" "$VAULT/.icontext/mcp/server.py" ".icontext/mcp/server.py" 1 # Copy connectors for connector in __init__.py base.py gmail.py linkedin.py; do - copy_file "$ICONTEXT_ROOT/connectors/$connector" "$VAULT/.icontext/connectors/$connector" ".icontext/connectors/$connector" + copy_file "$FBRAIN_ROOT/connectors/$connector" "$VAULT/.icontext/connectors/$connector" ".icontext/connectors/$connector" done - copy_file "$ICONTEXT_ROOT/cli.py" "$VAULT/.icontext/cli.py" ".icontext/cli.py" 1 + copy_file "$FBRAIN_ROOT/cli.py" "$VAULT/.icontext/cli.py" ".icontext/cli.py" 1 if [ "$MODE" = "standard" ] || [ "$MODE" = "agents" ]; then for hook in pre-commit pre-push post-commit; do - write_symlink "$ICONTEXT_ROOT/hooks/$hook" "$VAULT/.git/hooks/$hook" ".git/hooks/$hook" + write_symlink "$FBRAIN_ROOT/hooks/$hook" "$VAULT/.git/hooks/$hook" ".git/hooks/$hook" done - copy_file "$ICONTEXT_ROOT/workflows/sensitivity.yml" "$VAULT/.github/workflows/icontext-sensitivity.yml" ".github/workflows/icontext-sensitivity.yml" + copy_file "$FBRAIN_ROOT/workflows/sensitivity.yml" "$VAULT/.github/workflows/icontext-sensitivity.yml" ".github/workflows/icontext-sensitivity.yml" fi write_marker @@ -261,14 +261,14 @@ install_actions if [ "$DRY_RUN" -eq 1 ]; then echo "" - echo "icontext: dry run complete; no files were changed" + echo "fbrain: dry run complete; no files were changed" exit 0 fi if [ "$YES" -eq 0 ]; then if [ ! -t 0 ]; then echo "" - echo "icontext: refusing to install non-interactively without --yes" + echo "fbrain: refusing to install non-interactively without --yes" exit 1 fi echo "" @@ -277,7 +277,7 @@ if [ "$YES" -eq 0 ]; then case "$answer" in y|Y|yes|YES) ;; *) - echo "icontext: cancelled" + echo "fbrain: cancelled" exit 1 ;; esac @@ -286,41 +286,43 @@ fi MANIFEST_LINES=() DO_WRITE=1 echo "" -echo "icontext: applying changes" +echo "fbrain: applying changes" install_actions if [ "$MODE" = "agents" ]; then - echo "icontext: installing agent integrations" - python3 "$VAULT/.icontext/scripts/install_claude_integration.py" --icontext-root "$ICONTEXT_ROOT" --repo "$VAULT" - # Symlink icontext CLI to PATH — but only if the target file exists. - # When called from `icontext init`, cli.py is copied to the vault above, so - # it always exists at this point. When called standalone before `icontext init` + echo "fbrain: installing agent integrations" + python3 "$VAULT/.icontext/scripts/install_claude_integration.py" --fbrain-root "$FBRAIN_ROOT" --repo "$VAULT" + # Symlink fbrain CLI to PATH, plus an icontext compatibility alias. + # When called from `fbrain init`, cli.py is copied to the vault above, so + # it always exists at this point. When called standalone before `fbrain init` # has run, the vault is a fresh git repo and cli.py may not be there yet. CLI_TARGET="$VAULT/.icontext/cli.py" if [ -f "$CLI_TARGET" ]; then mkdir -p "$HOME/.local/bin" + ln -sf "$CLI_TARGET" "$HOME/.local/bin/fbrain" ln -sf "$CLI_TARGET" "$HOME/.local/bin/icontext" chmod +x "$CLI_TARGET" - echo "icontext: CLI available at ~/.local/bin/icontext" + echo "fbrain: CLI available at ~/.local/bin/fbrain" else - # Fall back to linking directly from the icontext source repo + # Fall back to linking directly from the fbrain source repo mkdir -p "$HOME/.local/bin" - ln -sf "$ICONTEXT_ROOT/cli.py" "$HOME/.local/bin/icontext" - chmod +x "$ICONTEXT_ROOT/cli.py" - echo "icontext: CLI linked from source at ~/.local/bin/icontext" - echo "icontext: (vault cli.py will be preferred after next install)" + ln -sf "$FBRAIN_ROOT/cli.py" "$HOME/.local/bin/fbrain" + ln -sf "$FBRAIN_ROOT/cli.py" "$HOME/.local/bin/icontext" + chmod +x "$FBRAIN_ROOT/cli.py" + echo "fbrain: CLI linked from source at ~/.local/bin/fbrain" + echo "fbrain: (vault cli.py will be preferred after next install)" fi fi echo "" -echo "icontext: install complete" +echo "fbrain: install complete" echo "" echo "Next steps:" echo " 1. Review .gitleaks.toml, .icontext-tiers.yml, .icontext/scripts, and .icontext/manifest.json" echo " 2. Build local search index: python3 .icontext/scripts/update_index.py --repo ." if [ "$MODE" != "agents" ]; then - echo " 3. Optional agent integrations: python3 .icontext/scripts/install_claude_integration.py --icontext-root $ICONTEXT_ROOT --repo $VAULT" - echo " 4. Verify everything: python3 .icontext/scripts/doctor.py --repo . --icontext-root $ICONTEXT_ROOT --deep" + echo " 3. Optional agent integrations: python3 .icontext/scripts/install_claude_integration.py --fbrain-root $FBRAIN_ROOT --repo $VAULT" + echo " 4. Verify everything: python3 .icontext/scripts/doctor.py --repo . --fbrain-root $FBRAIN_ROOT --deep" else - echo " 3. Verify everything: python3 .icontext/scripts/doctor.py --repo . --icontext-root $ICONTEXT_ROOT --deep" + echo " 3. Verify everything: python3 .icontext/scripts/doctor.py --repo . --fbrain-root $FBRAIN_ROOT --deep" fi diff --git a/launch/COPY.md b/launch/COPY.md index 95ba83c..55ec25e 100644 --- a/launch/COPY.md +++ b/launch/COPY.md @@ -1,4 +1,4 @@ -# iContext Launch Copy Package +# fbrain Launch Copy Package --- @@ -6,31 +6,31 @@ **Title:** ``` -Show HN: iContext – a folder + skills that give every AI agent the same brain +Show HN: fbrain – a folder + skills that give every AI agent the same brain ``` **Body:** -I built iContext because I was tired of re-explaining myself to Claude at the start of every session. "I'm building an AI app platform, I work with a team of three, here's my tech stack, here's what we decided last week..." Every single time. +I built fbrain because I was tired of re-explaining myself to Claude at the start of every session. "I'm building an AI app platform, I work with a team of three, here's my tech stack, here's what we decided last week..." Every single time. The fix I wanted: Claude already knows who I am before I type anything. But I didn't want to depend on an external API or a hosted service to get there. -iContext is two things: a folder (`~/context/`) and a set of skills that your AI agents read. When you run `icontext init`, it creates the folder structure and installs skill files into `~/.claude/skills/` and `~/.cursor/rules/`. From that point on, when you open Claude Code and say "populate my icontext profile", Claude uses its own Gmail MCP and browser tools to build your profile — no external synthesis step, no API keys required. +fbrain is two things: a folder (`~/context/`) and a set of skills that your AI agents read. When you run `fbrain init`, it creates the folder structure and installs skill files into `~/.claude/skills/` and `~/.cursor/rules/`. From that point on, when you open Claude Code and say "populate my fbrain profile", Claude uses its own Gmail MCP and browser tools to build your profile — no external synthesis step, no API keys required. The profile lives entirely on your machine: markdown files in three tiers (shareable, internal, vault). git-crypt encrypts the vault tier. gitleaks runs pre-commit. A doctor command tells you what's working. Since every agent reads the same folder, Claude Code, Cursor, and Codex all start from the same shared context layer. You set it up once. -Gemini-based headless sync is also available (`pip install icontext[sync]`) for CI or no-agent setups, but it's a fallback — not the default path. +Gemini-based headless sync is also available (`pip install fbrain[sync]`) for CI or no-agent setups, but it's a fallback — not the default path. Install: ``` -curl -fsSL icontext.floom.dev/install | bash -icontext init +curl -fsSL floom.dev/fbrain/install | bash +fbrain init ``` -Then: open Claude Code and say "Populate my icontext profile." +Then: open Claude Code and say "Populate my fbrain profile." -Repo: https://github.com/floomhq/icontext +Repo: https://github.com/floomhq/fbrain Curious what context sources other people would want connected. Gmail and your own description are the starting points, but there's a lot more that could feed into this. @@ -39,7 +39,7 @@ Curious what context sources other people would want connected. Gmail and your o ## 2. Twitter/X Single Launch Tweet ``` -Claude Code has been asking "what are you working on?" every single session. I fixed it. icontext is a folder + skills. Your agents share a brain. Two commands, no API key: https://github.com/floomhq/icontext #buildinpublic +Claude Code has been asking "what are you working on?" every single session. I fixed it. fbrain is a folder + skills. Your agents share a brain. Two commands, no API key: https://github.com/floomhq/fbrain #buildinpublic ``` *(267 chars)* @@ -54,7 +54,7 @@ Claude Code doesn't remember you. Every session starts cold. I got tired of re-explaining my projects, my team, and my context from scratch every morning. -So I built icontext. Thread: +So I built fbrain. Thread: ``` **Tweet 2 (problem, part 1):** @@ -81,15 +81,15 @@ There was no good middle ground. Until now. **Tweet 4 (solution, setup):** ``` -icontext is a folder and a set of skills. +fbrain is a folder and a set of skills. Two commands: ```bash -curl -fsSL icontext.floom.dev/install | bash -icontext init +curl -fsSL floom.dev/fbrain/install | bash +fbrain init ``` -Then open Claude Code and say: "Populate my icontext profile." +Then open Claude Code and say: "Populate my fbrain profile." Claude reads your Gmail via its own MCP. No external API calls. No API key needed. ``` @@ -115,14 +115,14 @@ Under the hood: - Three-tier vault: shareable / internal / encrypted vault - gitleaks + git-crypt block secrets before they hit git - Gemini headless sync available as a fallback for CI/no-agent setups -- `icontext doctor --deep` tells you exactly what's working +- `fbrain doctor --deep` tells you exactly what's working ``` **Tweet 7 (CTA):** ``` Open source. -https://github.com/floomhq/icontext +https://github.com/floomhq/fbrain If you use Claude Code daily and re-explain yourself every session, try this. Two commands to install. @@ -141,21 +141,21 @@ One folder, every agent. Your AI tools now share a brain. **Description:** ``` -Stop re-explaining yourself every Claude session. icontext installs a folder + skills into Claude Code, Cursor, and Codex. Your agents read from it, write to it, and keep it current — using their own tools. Local-first, no API keys, open source. +Stop re-explaining yourself every Claude session. fbrain installs a folder + skills into Claude Code, Cursor, and Codex. Your agents read from it, write to it, and keep it current — using their own tools. Local-first, no API keys, open source. ``` *(245 chars)* **First Comment from Maker:** -Hey, I'm Federico, the builder behind iContext. +Hey, I'm Federico, the builder behind fbrain. I've been running Claude Code daily for months. Every morning: new session, blank slate, re-explain the project, re-explain the team, re-explain what we decided two days ago. It's a small tax per session but it compounds badly. I tried hand-writing CLAUDE.md files, but that's maintenance work. The real context — who I'm working with, what we're building, what decisions shaped the project — lives in my Gmail threads and in my own head. Too sensitive to paste raw, too important to leave invisible. -The solution I wanted was for the AI to figure it out itself. So that's what iContext does. +The solution I wanted was for the AI to figure it out itself. So that's what fbrain does. -Two commands install a folder structure (`~/context/`) and a set of skill files into Claude Code and Cursor. When you ask Claude to "populate my icontext profile", it cascades through real sources on your behalf: Gmail via its own MCP, your browser if available, or a short self-description if neither works. The synthesis happens inside your Claude Code session. Nothing leaves your machine by default. +Two commands install a folder structure (`~/context/`) and a set of skill files into Claude Code and Cursor. When you ask Claude to "populate my fbrain profile", it cascades through real sources on your behalf: Gmail via its own MCP, your browser if available, or a short self-description if neither works. The synthesis happens inside your Claude Code session. Nothing leaves your machine by default. From that point, Claude Code, Cursor, and Codex all read from the same folder. One context layer. Every agent pre-loaded. @@ -178,7 +178,7 @@ You open a terminal, you start a new chat, and Claude has no idea who you are, w I got frustrated enough to build a fix. -It's called iContext. Two commands to install. No API key. When you're done, you open Claude Code and say: "Populate my icontext profile." Claude uses its own Gmail MCP to read your recent emails, figures out your projects and collaborators, and writes a structured profile to a local folder. +It's called fbrain. Two commands to install. No API key. When you're done, you open Claude Code and say: "Populate my fbrain profile." Claude uses its own Gmail MCP to read your recent emails, figures out your projects and collaborators, and writes a structured profile to a local folder. From that point on, every Claude Code session, every Cursor conversation, every Codex run starts pre-loaded with who you are and what you're working on. They all read from the same folder. One source of truth. @@ -186,10 +186,10 @@ The whole thing runs locally. No server, no cloud sync. The context folder is sp Works with Claude Code, Cursor, and Codex. Two install commands: -curl -fsSL icontext.floom.dev/install | bash -icontext init +curl -fsSL floom.dev/fbrain/install | bash +fbrain init -Open source, on GitHub: https://github.com/floomhq/icontext +Open source, on GitHub: https://github.com/floomhq/fbrain If you use AI tools daily and you're tired of re-explaining yourself every session, this is worth five minutes. @@ -206,9 +206,9 @@ I built a tool that gives Claude Code persistent memory using its own skills + C **Body:** -I was losing too much time re-explaining my working context at the start of every Claude session. So I built iContext to fix it. +I was losing too much time re-explaining my working context at the start of every Claude session. So I built fbrain to fix it. -It works with Claude's existing skills system. Run `icontext init` and it installs skill files into `~/.claude/skills/` and writes a small `CLAUDE.md` that points at `~/context/`. From that point on, when you open Claude Code and say "Populate my icontext profile", Claude reads your Gmail via its own Gmail MCP, figures out your projects and relationships, and writes the profile itself. +It works with Claude's existing skills system. Run `fbrain init` and it installs skill files into `~/.claude/skills/` and writes a small `CLAUDE.md` that points at `~/context/`. From that point on, when you open Claude Code and say "Populate my fbrain profile", Claude reads your Gmail via its own Gmail MCP, figures out your projects and relationships, and writes the profile itself. No external API calls. No Gemini key. The synthesis happens inside your Claude session. @@ -218,11 +218,11 @@ Also works with Cursor (installs into `.cursor/rules/`) and Codex (via optional Install: ``` -curl -fsSL icontext.floom.dev/install | bash -icontext init +curl -fsSL floom.dev/fbrain/install | bash +fbrain init ``` -Repo: https://github.com/floomhq/icontext +Repo: https://github.com/floomhq/fbrain I'd love feedback from daily Claude Code users. What context sources would you want connected next? @@ -232,14 +232,14 @@ I'd love feedback from daily Claude Code users. What context sources would you w **Title:** ``` -iContext: local-first context vault for AI coding agents (Claude Code, Codex, Cursor) — agent-populated, no external API by default +fbrain: local-first context vault for AI coding agents (Claude Code, Codex, Cursor) — agent-populated, no external API by default ``` **Body:** -Built iContext to solve a problem I had: AI coding tools forget you between sessions. +Built fbrain to solve a problem I had: AI coding tools forget you between sessions. -The new architecture is agent-driven: `icontext init` installs skill files into `~/.claude/skills/` and `~/.cursor/rules/`. When you ask your agent to populate the profile, it uses its own tools (Gmail MCP, browser) to build it. No external API calls by default. The synthesis happens inside the agent session. +The new architecture is agent-driven: `fbrain init` installs skill files into `~/.claude/skills/` and `~/.cursor/rules/`. When you ask your agent to populate the profile, it uses its own tools (Gmail MCP, browser) to build it. No external API calls by default. The synthesis happens inside the agent session. Architecture highlights for this crowd: - No cloud sync, no hosted DB @@ -251,7 +251,7 @@ Architecture highlights for this crowd: Currently works with Claude Code (skills + CLAUDE.md), Cursor (rules), and Codex/OpenCode (MCP). -Repo: https://github.com/floomhq/icontext +Repo: https://github.com/floomhq/fbrain Curious whether people here are using local embedding models for retrieval. The current FTS approach works well, but semantic reranking is on the roadmap. @@ -270,8 +270,8 @@ Curious whether people here are using local embedding models for retrieval. The The "Show HN" format with a factual one-line description performs best. Avoid superlatives, avoid questions in the title, avoid implying this is unprecedented. The title above is in the right register: factual, specific, no hype. Alternative titles if you want to test: -- "Show HN: iContext – open-source context vault for AI coding agents, agent-populated" -- "Show HN: iContext – skills + folder that give Claude Code persistent memory" +- "Show HN: fbrain – open-source context vault for AI coding agents, agent-populated" +- "Show HN: fbrain – skills + folder that give Claude Code persistent memory" **First 2 hours:** @@ -284,7 +284,7 @@ Alternative titles if you want to test: **How to handle negative comments:** - Assume good faith. Most HN criticism is genuine. -- For "why not just use CLAUDE.md": explain that iContext creates the folder structure, installs skills, handles encryption and secret scanning, and works across multiple agents. That's a different scope. +- For "why not just use CLAUDE.md": explain that fbrain creates the folder structure, installs skills, handles encryption and secret scanning, and works across multiple agents. That's a different scope. - For "why does it need Gmail access": the Gmail MCP access is scoped to metadata (subjects, senders). No message bodies leave the agent session. The user controls which MCP tools their agent can use. - For "this is a security risk": the doctor command and three-tier vault system exist specifically for this concern. Link to the SECURITY.md. - Never be defensive. HN can smell it. "That's a fair concern, here's how I thought about it" always lands better than "actually this is not a problem because..." @@ -299,13 +299,13 @@ Ranked roughly by virality potential (1 = most shareable): 2. **"Your AI agents don't share a brain. They should. Two commands to fix that."** -3. **"icontext is a folder. Your AI agents read it, write to it, and keep it current. That's the whole product."** +3. **"fbrain is a folder. Your AI agents read it, write to it, and keep it current. That's the whole product."** 4. **"The missing layer between your private context and your AI tools: a local folder with skills."** -5. **"CLAUDE.md is a hand-written lie. icontext builds your context from real data, using your agent's own tools."** +5. **"CLAUDE.md is a hand-written lie. fbrain builds your context from real data, using your agent's own tools."** -6. **"After icontext: open Claude, ask 'what do you know about me?' and it already knows."** +6. **"After fbrain: open Claude, ask 'what do you know about me?' and it already knows."** 7. **"Every AI coding tool has the same cold-start problem. One folder + skills fixes all of them at once."** diff --git a/launch/codex-review.md b/launch/codex-review.md index c5b9c33..3980779 100644 --- a/launch/codex-review.md +++ b/launch/codex-review.md @@ -1,4 +1,4 @@ -# iContext Synthesis Pipeline — Codex Review +# fbrain Synthesis Pipeline — Codex Review *Model: gpt-5.5 | Date: 2026-05-02 | Tokens used: Pass 1 ~59K, Pass 2 ~50K* --- diff --git a/launch/key-architecture-RECOMMENDATION.md b/launch/key-architecture-RECOMMENDATION.md index 44652d7..2d9f254 100644 --- a/launch/key-architecture-RECOMMENDATION.md +++ b/launch/key-architecture-RECOMMENDATION.md @@ -17,7 +17,7 @@ Keep `git-crypt`. Export the symmetric key, store it as a secure file attachment 3. Confirm 1Password account hardening: Secret Key on file, hardware security key (YubiKey) registered for account auth, recovery code stored offline, trusted devices reviewed. 4. Run a recovery drill: clone the repo into a temp dir, restore the key from 1Password, run `git-crypt unlock`, confirm `git-crypt status -e` shows decrypted content and `git status --short` is clean. 5. Delete the loose key file from `$HOME` after the drill. -6. On AX41: run the sparse-checkout + tripwire commands so `vault/` cannot be committed locally. Confirm AX41's GitHub credential for the icontext repo is read-only (deploy key) or removed. +6. On AX41: run the sparse-checkout + tripwire commands so `vault/` cannot be committed locally. Confirm AX41's GitHub credential for the fbrain repo is read-only (deploy key) or removed. ## What's automatable (agent can do once authorized) diff --git a/launch/linkedin-card.html b/launch/linkedin-card.html index 58370d6..e6dbdf4 100644 --- a/launch/linkedin-card.html +++ b/launch/linkedin-card.html @@ -3,7 +3,7 @@ - icontext Floom Launch Card + fbrain Floom Launch Card -
+
floom.

AI does not need a bigger prompt. It needs memory.

-

icontext gives your AI tools a safe place to find project context without exposing everything.

+

fbrain gives your AI tools a safe place to find project context without exposing everything.

-
$ icontext
+
$ fbrain
safe
@@ -250,7 +250,7 @@

AI does not need a bigger prompt. It needs memory.

diff --git a/launch/linkedin-post.txt b/launch/linkedin-post.txt index a72fe1d..f35c2c8 100644 --- a/launch/linkedin-post.txt +++ b/launch/linkedin-post.txt @@ -26,7 +26,7 @@ That is not agentic work. That is manual onboarding. -So I built icontext. +So I built fbrain. Plain English: @@ -49,4 +49,4 @@ Bigger prompts were the wrong answer. Safer context is the unlock. Open source: -https://github.com/floomhq/icontext +https://github.com/floomhq/fbrain diff --git a/launch/vaultwarden-backup-DEPLOYED.md b/launch/vaultwarden-backup-DEPLOYED.md index d1650cf..017e5fc 100644 --- a/launch/vaultwarden-backup-DEPLOYED.md +++ b/launch/vaultwarden-backup-DEPLOYED.md @@ -109,7 +109,7 @@ Caveat: Vaultwarden currently has 0 users, 0 ciphers — the drill cannot prove ## Adversarial review (Codex v2) — top 5 weaknesses + fixes -The original Codex v1 plan was reviewed by a fresh Codex invocation. The full v2 plan is at `/tmp/icontext-audit/launch/vaultwarden-backup-plan-v2.md`. Top findings: +The original Codex v1 plan was reviewed by a fresh Codex invocation. The full v2 plan is at `/tmp/fbrain-audit/launch/vaultwarden-backup-plan-v2.md`. Top findings: 1. **Single paper key was a recursive recovery failure.** Original v1 deferred Shamir fallback. v2 ships with both primary AND escrow age identities (two independent paper trails), with Shamir 2-of-3 documented as the next-quarter improvement. 2. **Cron is fragile vs systemd timers.** v2 uses systemd timers with `Persistent=true` + `flock` lock + `systemd-inhibit` shutdown delay. Implemented. @@ -189,9 +189,9 @@ When Federico finishes the 5 actions above, the score is 10/10. The pipeline is ## Files index -- `/tmp/icontext-audit/launch/vaultwarden-backup-plan.md` — original Codex v1 plan -- `/tmp/icontext-audit/launch/vaultwarden-backup-plan-v2.md` — Codex v2 adversarial rewrite -- `/tmp/icontext-audit/launch/vaultwarden-backup-DEPLOYED.md` — this file +- `/tmp/fbrain-audit/launch/vaultwarden-backup-plan.md` — original Codex v1 plan +- `/tmp/fbrain-audit/launch/vaultwarden-backup-plan-v2.md` — Codex v2 adversarial rewrite +- `/tmp/fbrain-audit/launch/vaultwarden-backup-DEPLOYED.md` — this file - `/root/fede-vault/infra/vaultwarden-backup-strategy.md` — original v1 (vault copy) - `/root/fede-vault/infra/vaultwarden-restore-drill.md` — drill procedure + first-run log - `/root/fede-vault/infra/vaultwarden-docker-compose.current.yml` — current `docker-compose.yml` diff --git a/launch/video-script-v2.txt b/launch/video-script-v2.txt index ff63a63..1fc1e59 100644 --- a/launch/video-script-v2.txt +++ b/launch/video-script-v2.txt @@ -1,4 +1,4 @@ -ICONTEXT LAUNCH VIDEO SCRIPT v2 +FBRAIN LAUNCH VIDEO SCRIPT v2 "AI has Alzheimer's, until it doesn't" Total runtime: ~75 seconds @@ -39,7 +39,7 @@ Visual: Cut to a terminal. Two commands running. Then the ~/context/ folder open "One folder. Three skill files. - That's icontext. Your agents read from it before answering. + That's fbrain. Your agents read from it before answering. They write back to it as they learn. Every agent. Same brain." @@ -51,7 +51,7 @@ Three quick visual cards or screen recordings. Each one beat. "One: one folder, every agent. Claude Code, Cursor, Codex — they all read the same ~/context/." Visual: Three terminals side by side, all loading context from the same folder. - "Two: it builds itself. Open Claude Code, say 'populate my icontext profile'. It reads your Gmail using its own tools. No API key. No server call." + "Two: it builds itself. Open Claude Code, say 'populate my fbrain profile'. It reads your Gmail using its own tools. No API key. No server call." Visual: Claude Code session running the populate skill, profile MD appearing. "Three: yours forever. Encrypted on your machine. Open source. You can read every line of what it knows about you." @@ -59,13 +59,13 @@ Three quick visual cards or screen recordings. Each one beat. [1:05 — 1:15] CTA -Visual: Install command big and clean. icontext.floom.dev URL. +Visual: Install command big and clean. floom.dev/fbrain URL. - "icontext. Open source. + "fbrain. Open source. Two commands. No API key. Your AI never forgets you again. - icontext.floom.dev" + floom.dev/fbrain" (end on the URL on screen, hold 2 seconds) diff --git a/mcp/server.py b/mcp/server.py index 69c72b1..5c305f6 100755 --- a/mcp/server.py +++ b/mcp/server.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Minimal stdio MCP server for icontext.""" +"""Minimal stdio MCP server for fbrain.""" from __future__ import annotations @@ -24,7 +24,7 @@ def _tools() -> list[dict]: return [ { "name": "search_vault", - "description": "Search the local icontext index.", + "description": "Search the local fbrain index.", "inputSchema": { "type": "object", "properties": { @@ -61,7 +61,7 @@ def _tools() -> list[dict]: }, { "name": "rebuild_index", - "description": "Rebuild the local icontext search index.", + "description": "Rebuild the local fbrain search index.", "inputSchema": {"type": "object", "properties": {}}, }, { @@ -103,7 +103,7 @@ def handle(self, method: str, params: dict | None) -> dict | None: return { "protocolVersion": "2024-11-05", "capabilities": {"tools": {}}, - "serverInfo": {"name": "icontext", "version": "0.1.0"}, + "serverInfo": {"name": "fbrain", "version": "0.5.0"}, } if method == "notifications/initialized": return None @@ -152,7 +152,7 @@ def call_tool(self, params: dict) -> dict: if name == "sync_source": source = args.get("source", "gmail") result = subprocess.run( - ["icontext", "sync", source, "--vault", str(self.repo)], + ["fbrain", "sync", source, "--vault", str(self.repo)], capture_output=True, text=True, timeout=300, ) output = result.stdout + result.stderr @@ -166,7 +166,7 @@ def call_tool(self, params: dict) -> dict: if name == "list_sources": cfg_path = self.repo / ".icontext" / "connectors.json" if not cfg_path.exists(): - return _content("No sources configured. Run: icontext connect gmail") + return _content("No sources configured. Run: fbrain connect gmail") cfg = json.loads(cfg_path.read_text()) lines = [] for src, data in cfg.items(): diff --git a/pyproject.toml b/pyproject.toml index 2b80b1e..627ef0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [build-system] requires = ["setuptools>=70"] -build-backend = "setuptools.backends.legacy:build" +build-backend = "setuptools.build_meta" [project] -name = "icontext" -version = "0.4.0" -description = "Persistent memory for AI coding tools — local, encrypted, built from your real data." +name = "fbrain" +version = "0.5.0" +description = "Persistent memory for AI coding tools: local, encrypted, built from your real data." readme = "README.md" license = {text = "MIT"} requires-python = ">=3.11" @@ -31,12 +31,14 @@ sync = ["google-generativeai>=0.8", "keyring>=25"] dev = ["pytest>=8", "google-generativeai>=0.8", "keyring>=25"] [project.urls] -Homepage = "https://icontext.dev" -Repository = "https://github.com/floomhq/icontext" -Issues = "https://github.com/floomhq/icontext/issues" +Homepage = "https://floom.dev/fbrain" +Repository = "https://github.com/floomhq/fbrain" +Issues = "https://github.com/floomhq/fbrain/issues" +"Legacy Homepage" = "https://icontext.floom.dev" [project.scripts] -icontext = "cli:main" +fbrain = "cli:main" +icontext = "cli:_deprecated_main" [tool.setuptools] py-modules = ["cli"] diff --git a/scripts/check_tiers.py b/scripts/check_tiers.py index 2a64b50..1e1b2cf 100755 --- a/scripts/check_tiers.py +++ b/scripts/check_tiers.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Check changed files against icontext tier placement.""" +"""Check changed files against fbrain tier placement.""" from __future__ import annotations @@ -194,24 +194,24 @@ def check_paths(repo: Path, config: Path, paths: list[str]) -> int: ) if warnings: - print("icontext: tier warnings:", file=sys.stderr) + print("fbrain: tier warnings:", file=sys.stderr) for warning in warnings: print(f" - {warning}", file=sys.stderr) if failures: - print("icontext: sensitivity tier check failed:", file=sys.stderr) + print("fbrain: sensitivity tier check failed:", file=sys.stderr) for failure in failures: print(f" - {failure}", file=sys.stderr) return 1 if skipped_unclassified: print( - "icontext: sensitivity tier check passed for " + "fbrain: sensitivity tier check passed for " f"{len(paths)} file(s); skipped {skipped_unclassified} unclassified " "legacy path(s)" ) else: - print(f"icontext: sensitivity tier check passed for {len(paths)} file(s)") + print(f"fbrain: sensitivity tier check passed for {len(paths)} file(s)") return 0 diff --git a/scripts/doctor.py b/scripts/doctor.py index c016fbe..8a3196a 100755 --- a/scripts/doctor.py +++ b/scripts/doctor.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Verify an icontext install end to end.""" +"""Verify a fbrain install end to end.""" from __future__ import annotations @@ -37,12 +37,12 @@ def __init__(self, repo: Path, icontext_root: Path, query: str, deep: bool): self.skills_first = self._detect_skills_first() def _detect_skills_first(self) -> bool: - """Skills-first mode is the default for `icontext init`. Signal: at - least one icontext skill installed in ~/.claude/skills/, AND no legacy + """Skills-first mode is the default for `fbrain init`. Signal: at + least one fbrain skill installed in ~/.claude/skills/, AND no legacy marker present in the vault.""" skills_dir = Path("~/.claude/skills").expanduser() has_skills = skills_dir.is_dir() and any( - (skills_dir / f"icontext-{name}/SKILL.md").is_file() + (skills_dir / f"fbrain-{name}/SKILL.md").is_file() for name in ("populate-profile", "refresh-profile", "share-card") ) legacy_marker = self.repo / ".icontext" / "manifest.json" @@ -213,7 +213,7 @@ def check_index(self) -> None: db = self.repo / ".git" / "icontext" / "index.sqlite" marker = self.repo / ".git" / "icontext" / "last-indexed" if not db.exists(): - self._legacy_check("index:sqlite", "fail", "missing — run: icontext rebuild") + self._legacy_check("index:sqlite", "fail", "missing — run: fbrain rebuild") return count = marker.read_text(encoding="utf-8").strip() if marker.exists() else "unknown" self.pass_("index:sqlite", f"{db} ({count} indexed text files)") @@ -230,7 +230,7 @@ def check_mcp_stdio(self) -> None: "params": { "protocolVersion": "2024-11-05", "capabilities": {}, - "clientInfo": {"name": "icontext-doctor", "version": "1"}, + "clientInfo": {"name": "fbrain-doctor", "version": "1"}, }, } ), @@ -270,7 +270,7 @@ def check_mcp_stdio(self) -> None: return text = responses[-1]["result"]["content"][0]["text"] if text == "[]": - self._legacy_check("mcp:search", "fail", f"no results for {self.query!r} — run: icontext rebuild") + self._legacy_check("mcp:search", "fail", f"no results for {self.query!r} — run: fbrain rebuild") else: self.pass_("mcp:search", f"{len(text)} response chars for {self.query!r}") @@ -279,7 +279,7 @@ def check_agent_configs(self) -> None: self._check_json_server( "claude:mcp", Path("~/.claude/.mcp.json").expanduser(), - ["mcpServers", "icontext"], + ["mcpServers", "fbrain"], expected_args, ) settings = Path("~/.claude/settings.json").expanduser() @@ -298,13 +298,13 @@ def check_agent_configs(self) -> None: self._check_json_server( "cursor:mcp", Path("~/.cursor/mcp.json").expanduser(), - ["mcpServers", "icontext"], + ["mcpServers", "fbrain"], expected_args, ) self._check_json_server( "opencode:mcp", Path("~/.config/opencode/opencode.json").expanduser(), - ["mcp", "icontext"], + ["mcp", "fbrain"], expected_args, command_key="command", command_in_args=True, @@ -329,9 +329,9 @@ def _check_json_server( actual = actual[1:] if actual == expected_args: self.pass_(name, str(path)) - elif self.skills_first and "icontext" in str(actual): - # Skills-first: each `icontext init` registers MCP for that vault. - # If icontext is registered at all, the user is wired up. + elif self.skills_first and ("fbrain" in str(actual) or "icontext" in str(actual)): + # Skills-first: each `fbrain init` registers MCP for that vault. + # If fbrain is registered at all, the user is wired up. self.pass_(name, f"{path} (registered for a different vault)") else: self._legacy_check(name, "fail", f"unexpected args: {actual}") @@ -342,7 +342,7 @@ def _check_codex(self, expected_args: list[str]) -> None: path = Path("~/.codex/config.toml").expanduser() try: data = tomllib.loads(path.read_text(encoding="utf-8")) - server = data["mcp_servers"]["icontext"] + server = data["mcp_servers"]["fbrain"] if server.get("command") == "python3" and server.get("args") == expected_args: self.pass_("codex:mcp", str(path)) elif self.skills_first and server.get("command") == "python3": @@ -353,24 +353,24 @@ def _check_codex(self, expected_args: list[str]) -> None: self._legacy_check("codex:mcp", "fail", str(exc)) def check_native_clients(self) -> None: - codex = self.command(["codex", "mcp", "get", "icontext"], cwd=Path.home(), timeout=15) + codex = self.command(["codex", "mcp", "get", "fbrain"], cwd=Path.home(), timeout=15) if codex.returncode == 0 and str(self.repo) in codex.stdout: - self.pass_("codex:native", "codex mcp get icontext") - elif self.skills_first and codex.returncode == 0 and "icontext" in codex.stdout: - self.pass_("codex:native", "codex has icontext registered (for a different vault)") + self.pass_("codex:native", "codex mcp get fbrain") + elif self.skills_first and codex.returncode == 0 and ("fbrain" in codex.stdout or "icontext" in codex.stdout): + self.pass_("codex:native", "codex has fbrain registered (for a different vault)") else: self._legacy_check("codex:native", "fail", codex.stdout.strip()) opencode = self.command(["opencode", "mcp", "list"], cwd=Path.home(), timeout=45) - if opencode.returncode == 0 and "icontext" in opencode.stdout and "connected" in opencode.stdout: - self.pass_("opencode:native", "opencode reports icontext connected") + if opencode.returncode == 0 and "fbrain" in opencode.stdout and "connected" in opencode.stdout: + self.pass_("opencode:native", "opencode reports fbrain connected") else: self.fail("opencode:native", opencode.stdout.strip()) if shutil.which("cursor-agent"): - cursor = self.command(["cursor-agent", "mcp", "list-tools", "icontext"], cwd=Path.home(), timeout=45) + cursor = self.command(["cursor-agent", "mcp", "list-tools", "fbrain"], cwd=Path.home(), timeout=45) if cursor.returncode == 0 and "search_vault" in cursor.stdout: - self.pass_("cursor:native", "cursor-agent lists icontext tools") + self.pass_("cursor:native", "cursor-agent lists fbrain tools") else: self.fail("cursor:native", cursor.stdout.strip()) elif shutil.which("cursor"): @@ -384,9 +384,9 @@ def check_native_clients(self) -> None: # ------------------------------------------------------------------ def check_connectors(self) -> None: - """Check that connector files are present in the iContext install root. + """Check that connector files are present in the fbrain install root. - In the skills-first architecture, connectors live in ~/icontext/connectors/ + In the skills-first architecture, connectors live in ~/fbrain/connectors/ (the install root), not inside the vault. The vault gets skills + folder structure; sync (which uses connectors) is opt-in and runs from the install. """ @@ -409,7 +409,7 @@ def check_connectors(self) -> None: else: self.fail("connectors:cli.py", f"{cli_path} missing") - symlink = Path("~/.local/bin/icontext").expanduser() + symlink = Path("~/.local/bin/fbrain").expanduser() if symlink.exists() or symlink.is_symlink(): target = symlink.resolve() if symlink.is_symlink() else symlink self.pass_("connectors:symlink", f"{symlink} -> {target}") @@ -423,7 +423,7 @@ def check_sources(self) -> None: cfg_path = self.repo / ".icontext" / "connectors.json" if not cfg_path.exists(): - self.warn("sources:config", "connectors.json not found — run: icontext connect gmail") + self.warn("sources:config", "connectors.json not found — run: fbrain connect gmail") return try: @@ -438,7 +438,7 @@ def check_sources(self) -> None: for source_name, source_cfg in cfg.items(): last_sync_str = source_cfg.get("last_sync") if not last_sync_str: - self.warn(f"sources:{source_name}:last_sync", "never synced — run: icontext sync") + self.warn(f"sources:{source_name}:last_sync", "never synced — run: fbrain sync") else: try: ts = datetime.fromisoformat(last_sync_str.replace("Z", "+00:00")) @@ -446,7 +446,7 @@ def check_sources(self) -> None: days = (datetime.now(UTC) - ts).days self.warn( f"sources:{source_name}:last_sync", - f"stale ({days}d ago) — run: icontext sync {source_name}", + f"stale ({days}d ago) — run: fbrain sync {source_name}", ) else: self.pass_(f"sources:{source_name}:last_sync", last_sync_str) @@ -464,7 +464,7 @@ def check_sources(self) -> None: else: self.warn( f"sources:{source_name}:profile", - f"{profile_path} missing — run: icontext sync {source_name}", + f"{profile_path} missing — run: fbrain sync {source_name}", ) def check_profile(self) -> None: @@ -483,28 +483,28 @@ def check_profile(self) -> None: if user_md.exists(): self.pass_("profile:user.md", str(user_md)) else: - self.fail("profile:user.md", f"{user_md} missing — run: icontext sync gmail") + self.fail("profile:user.md", f"{user_md} missing — run: fbrain sync gmail") if "linkedin" in cfg: linkedin_md = self.repo / "internal" / "profile" / "linkedin.md" if linkedin_md.exists(): self.pass_("profile:linkedin.md", str(linkedin_md)) else: - self.fail("profile:linkedin.md", f"{linkedin_md} missing — run: icontext sync linkedin") + self.fail("profile:linkedin.md", f"{linkedin_md} missing — run: fbrain sync linkedin") if cfg: card_md = self.repo / "shareable" / "profile" / "context-card.md" if card_md.exists(): self.pass_("profile:context-card.md", str(card_md)) else: - self.warn("profile:context-card.md", f"{card_md} missing — run: icontext sync") + self.warn("profile:context-card.md", f"{card_md} missing — run: fbrain sync") def check_environment(self) -> None: """Check optional headless-sync deps. Default install requires none of these.""" import importlib # All checks here are warn-level: the default flow uses agent skills, not Gemini. - # These deps only matter for the optional `icontext sync` headless fallback. + # These deps only matter for the optional `fbrain sync` headless fallback. gemini_key = os.environ.get("GEMINI_API_KEY") google_key = os.environ.get("GOOGLE_API_KEY") if gemini_key: @@ -523,7 +523,7 @@ def check_environment(self) -> None: except ImportError: self.pass_( "env:google-generativeai", - "not installed — install only if you want headless sync: pip install 'icontext[sync]'", + "not installed — install only if you want headless sync: pip install 'fbrain[sync]'", ) try: @@ -532,39 +532,39 @@ def check_environment(self) -> None: except ImportError: self.pass_( "env:keyring", - "not installed — install only if you want headless sync: pip install 'icontext[sync]'", + "not installed — install only if you want headless sync: pip install 'fbrain[sync]'", ) # Skill files installed? skills_dir = Path("~/.claude/skills").expanduser() - skill_names = ("icontext-populate-profile", "icontext-refresh-profile", "icontext-share-card") + skill_names = ("fbrain-populate-profile", "fbrain-refresh-profile", "fbrain-share-card") missing = [n for n in skill_names if not (skills_dir / n / "SKILL.md").exists()] if not missing: self.pass_("skills:claude", f"all 3 skills installed at {skills_dir}") else: - self.fail("skills:claude", f"missing skills: {', '.join(missing)} — run: icontext init") + self.fail("skills:claude", f"missing skills: {', '.join(missing)} — run: fbrain init") def check_claude_integration(self) -> None: """Check CLAUDE.md snippet and .mcp.json entry.""" claude_md = Path("~/.claude/CLAUDE.md").expanduser() if not claude_md.exists(): - self.warn("claude:CLAUDE.md", f"{claude_md} not found — run: icontext init") - elif "" in claude_md.read_text(encoding="utf-8", errors="replace"): - self.pass_("claude:CLAUDE.md", "icontext snippet present") + self.warn("claude:CLAUDE.md", f"{claude_md} not found — run: fbrain init") + elif "" in claude_md.read_text(encoding="utf-8", errors="replace"): + self.pass_("claude:CLAUDE.md", "fbrain snippet present") else: - self.fail("claude:CLAUDE.md", " snippet missing — run: icontext init") + self.fail("claude:CLAUDE.md", " snippet missing — run: fbrain init") mcp_json = Path("~/.claude/.mcp.json").expanduser() if not mcp_json.exists(): - self.warn("claude:mcp.json", f"{mcp_json} not found — run: icontext init") + self.warn("claude:mcp.json", f"{mcp_json} not found — run: fbrain init") else: try: data = json.loads(mcp_json.read_text(encoding="utf-8")) servers = data.get("mcpServers", {}) - if "icontext" in servers: - self.pass_("claude:mcp.json", "icontext server entry present") + if "fbrain" in servers: + self.pass_("claude:mcp.json", "fbrain server entry present") else: - self.fail("claude:mcp.json", "icontext entry missing — run: icontext init") + self.fail("claude:mcp.json", "fbrain entry missing — run: fbrain init") except Exception as exc: self.fail("claude:mcp.json", f"cannot parse {mcp_json}: {exc}") @@ -593,7 +593,7 @@ def check_github_action(self) -> None: "run", "list", "--workflow", - "icontext sensitivity", + "fbrain sensitivity", "--limit", "1", "--json", @@ -643,7 +643,7 @@ def run(self) -> int: self.check_inputs() if any(check.status == "fail" for check in self.checks): return 1 - with tempfile.TemporaryDirectory(prefix="icontext-doctor-") as tempdir: + with tempfile.TemporaryDirectory(prefix="fbrain-doctor-") as tempdir: temp_root = Path(tempdir) dry_repo = temp_root / "dry-run-repo" real_repo = temp_root / "real-repo" @@ -667,7 +667,12 @@ def command( timeout: int = 30, extra_env: dict[str, str] | None = None, ) -> subprocess.CompletedProcess: - env = {**os.environ, "ICONTEXT_ROOT": str(self.icontext_root), "VAULT": str(cwd)} + env = { + **os.environ, + "FBRAIN_ROOT": str(self.icontext_root), + "ICONTEXT_ROOT": str(self.icontext_root), + "VAULT": str(cwd), + } if extra_env: env.update(extra_env) return subprocess.run( @@ -852,7 +857,7 @@ def manifest_absolute_fields(self, data: object) -> list[str]: if not isinstance(data, dict): return [] fields: list[str] = [] - for key in ["icontext_root", "vault"]: + for key in ["fbrain_root", "icontext_root", "vault"]: value = data.get(key) if isinstance(value, str) and Path(value).is_absolute(): fields.append(key) @@ -919,7 +924,7 @@ def print_text(checks: list[Check]) -> None: def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--repo", default="~/context") - parser.add_argument("--icontext-root", default="~/icontext") + parser.add_argument("--fbrain-root", "--icontext-root", dest="fbrain_root", default="~/fbrain") parser.add_argument("--query", default="profile context") parser.add_argument("--deep", action="store_true", help="run slower gitleaks and GitHub checks") parser.add_argument("--fresh-install", action="store_true", help="verify install.sh and uninstall.sh in temp git repos") @@ -927,9 +932,9 @@ def main() -> int: args = parser.parse_args() if args.fresh_install: - doctor = FreshInstallDoctor(Path(args.icontext_root)) + doctor = FreshInstallDoctor(Path(args.fbrain_root)) else: - doctor = Doctor(Path(args.repo), Path(args.icontext_root), args.query, args.deep) + doctor = Doctor(Path(args.repo), Path(args.fbrain_root), args.query, args.deep) exit_code = doctor.run() if args.json: print(json.dumps([check.__dict__ for check in doctor.checks], indent=2)) diff --git a/scripts/eval_retrieval.py b/scripts/eval_retrieval.py index eb9dd51..68705cb 100755 --- a/scripts/eval_retrieval.py +++ b/scripts/eval_retrieval.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Evaluate icontext retrieval against explicit query/path cases.""" +"""Evaluate fbrain retrieval against explicit query/path cases.""" from __future__ import annotations diff --git a/scripts/icontext_classify.py b/scripts/icontext_classify.py index c48efe8..b2c2960 100755 --- a/scripts/icontext_classify.py +++ b/scripts/icontext_classify.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Rule-based sensitivity classifier for icontext. +"""Rule-based sensitivity classifier for fbrain. The classifier is intentionally deterministic. It is the local safety net used by hooks and CI; LLM review can be layered on top later without making pushes diff --git a/scripts/indexlib.py b/scripts/indexlib.py index a547885..679bcb0 100755 --- a/scripts/indexlib.py +++ b/scripts/indexlib.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Local SQLite FTS index for icontext vaults.""" +"""Local SQLite FTS index for fbrain vaults.""" from __future__ import annotations diff --git a/scripts/install_claude_integration.py b/scripts/install_claude_integration.py index 9e405f5..fc4d8fe 100755 --- a/scripts/install_claude_integration.py +++ b/scripts/install_claude_integration.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Install icontext MCP and prompt integrations into local coding agents.""" +"""Install fbrain MCP and prompt integrations into local coding agents.""" from __future__ import annotations @@ -21,33 +21,33 @@ def _write_json(path: Path, data: dict) -> None: path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") -def _server_script(icontext_root: Path) -> str: - return str(icontext_root / "mcp" / "server.py") +def _server_script(fbrain_root: Path) -> str: + return str(fbrain_root / "mcp" / "server.py") def _json_string(value: str) -> str: return json.dumps(value) -def install_claude_mcp(claude_dir: Path, icontext_root: Path, repo: Path) -> None: +def install_claude_mcp(claude_dir: Path, fbrain_root: Path, repo: Path) -> None: path = claude_dir / ".mcp.json" data = _read_json(path) servers = data.setdefault("mcpServers", {}) - servers["icontext"] = { + servers["fbrain"] = { "command": "python3", - "args": [_server_script(icontext_root), "--repo", str(repo)], + "args": [_server_script(fbrain_root), "--repo", str(repo)], } _write_json(path, data) -def install_claude_hook(claude_dir: Path, icontext_root: Path, repo: Path) -> None: +def install_claude_hook(claude_dir: Path, fbrain_root: Path, repo: Path) -> None: path = claude_dir / "settings.json" data = _read_json(path) hooks = data.setdefault("hooks", {}) entries = hooks.setdefault("UserPromptSubmit", []) command = ( - f"ICONTEXT_ROOT={icontext_root} ICONTEXT_VAULT={repo} " - f"{icontext_root / 'hooks' / 'user-prompt-submit'}" + f"FBRAIN_ROOT={fbrain_root} FBRAIN_VAULT={repo} " + f"{fbrain_root / 'hooks' / 'user-prompt-submit'}" ) desired = { "matcher": "", @@ -69,38 +69,38 @@ def install_claude_hook(claude_dir: Path, icontext_root: Path, repo: Path) -> No _write_json(path, data) -def install_claude(claude_dir: Path, icontext_root: Path, repo: Path) -> None: - install_claude_mcp(claude_dir, icontext_root, repo) - install_claude_hook(claude_dir, icontext_root, repo) +def install_claude(claude_dir: Path, fbrain_root: Path, repo: Path) -> None: + install_claude_mcp(claude_dir, fbrain_root, repo) + install_claude_hook(claude_dir, fbrain_root, repo) -def install_cursor(cursor_mcp: Path, icontext_root: Path, repo: Path) -> None: +def install_cursor(cursor_mcp: Path, fbrain_root: Path, repo: Path) -> None: data = _read_json(cursor_mcp) servers = data.setdefault("mcpServers", {}) - servers["icontext"] = { + servers["fbrain"] = { "command": "python3", - "args": [_server_script(icontext_root), "--repo", str(repo)], + "args": [_server_script(fbrain_root), "--repo", str(repo)], "env": {}, } _write_json(cursor_mcp, data) -def install_opencode(opencode_config: Path, icontext_root: Path, repo: Path) -> None: +def install_opencode(opencode_config: Path, fbrain_root: Path, repo: Path) -> None: data = _read_json(opencode_config) servers = data.setdefault("mcp", {}) - servers["icontext"] = { + servers["fbrain"] = { "type": "local", - "command": ["python3", _server_script(icontext_root), "--repo", str(repo)], + "command": ["python3", _server_script(fbrain_root), "--repo", str(repo)], "enabled": True, } _write_json(opencode_config, data) -def _codex_block(icontext_root: Path, repo: Path) -> str: - args = ", ".join(_json_string(arg) for arg in [_server_script(icontext_root), "--repo", str(repo)]) +def _codex_block(fbrain_root: Path, repo: Path) -> str: + args = ", ".join(_json_string(arg) for arg in [_server_script(fbrain_root), "--repo", str(repo)]) return "\n".join( [ - "[mcp_servers.icontext]", + "[mcp_servers.fbrain]", 'command = "python3"', f"args = [{args}]", "", @@ -108,18 +108,18 @@ def _codex_block(icontext_root: Path, repo: Path) -> str: ) -def install_codex(codex_config: Path, icontext_root: Path, repo: Path) -> None: +def install_codex(codex_config: Path, fbrain_root: Path, repo: Path) -> None: codex_config.parent.mkdir(parents=True, exist_ok=True) text = codex_config.read_text(encoding="utf-8") if codex_config.exists() else "" - block = _codex_block(icontext_root, repo) - pattern = re.compile(r"(?ms)^\[mcp_servers\.icontext\]\n.*?(?=^\[|\Z)") + block = _codex_block(fbrain_root, repo) + pattern = re.compile(r"(?ms)^\[mcp_servers\.(?:fbrain|icontext)\]\n.*?(?=^\[|\Z)") if pattern.search(text): text = pattern.sub(block, text) else: text = text.rstrip() + "\n\n" + block parsed = tomllib.loads(text) - if parsed["mcp_servers"]["icontext"]["args"][-1] != str(repo): - raise ValueError("Codex icontext MCP config did not round-trip") + if parsed["mcp_servers"]["fbrain"]["args"][-1] != str(repo): + raise ValueError("Codex fbrain MCP config did not round-trip") codex_config.write_text(text, encoding="utf-8") @@ -129,7 +129,7 @@ def main() -> int: parser.add_argument("--codex-config", default="~/.codex/config.toml") parser.add_argument("--cursor-mcp", default="~/.cursor/mcp.json") parser.add_argument("--opencode-config", default="~/.config/opencode/opencode.json") - parser.add_argument("--icontext-root", default="~/icontext") + parser.add_argument("--fbrain-root", "--icontext-root", dest="fbrain_root", default="~/fbrain") parser.add_argument("--repo", default="~/context") parser.add_argument( "--agents", @@ -143,22 +143,22 @@ def main() -> int: codex_config = Path(args.codex_config).expanduser() cursor_mcp = Path(args.cursor_mcp).expanduser() opencode_config = Path(args.opencode_config).expanduser() - icontext_root = Path(args.icontext_root).expanduser().resolve() + fbrain_root = Path(args.fbrain_root).expanduser().resolve() repo = Path(args.repo).expanduser().resolve() requested = set(args.agents) if "all" in requested: requested = {"claude", "codex", "cursor", "opencode"} if "claude" in requested: - install_claude(claude_dir, icontext_root, repo) + install_claude(claude_dir, fbrain_root, repo) if "codex" in requested: - install_codex(codex_config, icontext_root, repo) + install_codex(codex_config, fbrain_root, repo) if "cursor" in requested: - install_cursor(cursor_mcp, icontext_root, repo) + install_cursor(cursor_mcp, fbrain_root, repo) if "opencode" in requested: - install_opencode(opencode_config, icontext_root, repo) + install_opencode(opencode_config, fbrain_root, repo) - print(f"icontext: installed {', '.join(sorted(requested))} integrations for {repo}") + print(f"fbrain: installed {', '.join(sorted(requested))} integrations for {repo}") return 0 diff --git a/scripts/prompt_context.py b/scripts/prompt_context.py index fbcff9e..dc58ac4 100755 --- a/scripts/prompt_context.py +++ b/scripts/prompt_context.py @@ -39,17 +39,21 @@ def _int_env(name: str, default: int, minimum: int, maximum: int) -> int: def main() -> int: payload = json.loads(sys.stdin.read() or "{}") - repo = Path(os.environ.get("ICONTEXT_VAULT", str(Path("~/context").expanduser()))) - max_tier = os.environ.get("ICONTEXT_MAX_TIER", DEFAULT_MAX_TIER).strip().lower() or DEFAULT_MAX_TIER - char_budget = _int_env("ICONTEXT_PROMPT_CHAR_BUDGET", DEFAULT_CHAR_BUDGET, 0, 6000) - limit = _int_env("ICONTEXT_PROMPT_LIMIT", DEFAULT_LIMIT, 1, 10) + repo = Path(os.environ.get("FBRAIN_VAULT") or os.environ.get("ICONTEXT_VAULT", str(Path("~/context").expanduser()))) + max_tier = (os.environ.get("FBRAIN_MAX_TIER") or os.environ.get("ICONTEXT_MAX_TIER", DEFAULT_MAX_TIER)).strip().lower() or DEFAULT_MAX_TIER + char_budget = _int_env("FBRAIN_PROMPT_CHAR_BUDGET", DEFAULT_CHAR_BUDGET, 0, 6000) + if "FBRAIN_PROMPT_CHAR_BUDGET" not in os.environ: + char_budget = _int_env("ICONTEXT_PROMPT_CHAR_BUDGET", DEFAULT_CHAR_BUDGET, 0, 6000) + limit = _int_env("FBRAIN_PROMPT_LIMIT", DEFAULT_LIMIT, 1, 10) + if "FBRAIN_PROMPT_LIMIT" not in os.environ: + limit = _int_env("ICONTEXT_PROMPT_LIMIT", DEFAULT_LIMIT, 1, 10) prompt = _prompt(payload) context = "" if prompt and repo.exists() and char_budget: results = search(repo, prompt, limit=limit, max_tier=max_tier) if results: - lines = ["Relevant context from icontext:"] + lines = ["Relevant context from fbrain:"] used_chars = len(lines[0]) seen_paths: set[str] = set() for result in results: diff --git a/scripts/update_index.py b/scripts/update_index.py index 59e351b..ce63940 100755 --- a/scripts/update_index.py +++ b/scripts/update_index.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Update the local icontext search index.""" +"""Update the local fbrain search index.""" from __future__ import annotations @@ -14,7 +14,7 @@ def main() -> int: args = parser.parse_args() indexed = rebuild(args.repo) - print(f"icontext: indexed {indexed} text file(s)") + print(f"fbrain: indexed {indexed} text file(s)") return 0 diff --git a/skills/fbrain-populate-profile/SKILL.md b/skills/fbrain-populate-profile/SKILL.md new file mode 100644 index 0000000..b58cb1a --- /dev/null +++ b/skills/fbrain-populate-profile/SKILL.md @@ -0,0 +1,197 @@ +--- +name: fbrain-populate-profile +description: > + Build the user's fbrain profile from real data sources. Use when the user + asks to "populate my profile", "build my context", "set up fbrain", or when + internal/profile/user.md is missing. Cascades through Gmail MCP -> browser + scrape -> PDF -> user-described, picking the highest-quality source available. +--- + +# fbrain: Populate Profile + +Build a structured user profile in `~/context/internal/profile/` from real-world data sources. Follow the cascade in order. Use the highest-quality source available. + +## Source cascade (try in order) + +### 1. Gmail (highest quality) + +If a Gmail MCP server is available (e.g. `mcp__gmail__*` or `mcp__claude_ai_Gmail__*` tools), use it: + +- Fetch last 90 days of sent + inbox metadata (subject, from, to, date, cc). +- DO NOT fetch message bodies. Headers only. +- Run the synthesis pipeline in the next section. + +### 2. LinkedIn (if available) + +If browser automation is available (Playwright MCP, claude-in-chrome, etc.): + +- Navigate to the user's LinkedIn profile (ask for the handle if you do not know it). +- Capture work history, education, skills, headline. + +If browser automation is not available: + +- Ask the user to "Save to PDF" their LinkedIn profile (linkedin.com/in/them -> More -> Save to PDF). +- Read the PDF text via the `pdf` skill or pypdf. + +### 3. User-described (lowest friction, lowest quality) + +If neither of the above works, ask the user 4 questions: + +- What are you working on right now? +- What companies/projects matter to you? +- Who do you collaborate with most? +- What is your background in 2-3 sentences? + +## Synthesis rules (apply to whatever data you collected) + +### Stage A: extract structured entities + +From the data, extract: + +**People** (only if there is evidence of a real human relationship): + +- name, email/handle, company, role +- evidence_messages: count of real messages (sent + received combined). Minimum threshold: >= 1 message involving a real named person (not a bot or service). Tier A (hot/warm) = 3+ messages or bidirectional contact. Tier B (cold) = 1-2 messages but clearly human and named. +- direction: mostly_inbound | mostly_outbound | balanced +- topics: 2-3 subject patterns + +**EXCLUDE these as people:** + +- SaaS welcome emails (welcome@, no-reply@, noreply@, team@ from a SaaS domain you only see once) +- Notification senders (notifications@, alerts@, support@, github-actions[bot], dependabot[bot]) +- Billing/payment processors (payments-noreply@, failed-payments@) unless they appear in active human threads +- Automated systems (Cal.com booking confirmations, npm publish notifications, Vercel deploy alerts) +- Anyone where the sender is clearly a bot, automated pipeline, or service account + +**Projects** (need >= 2 evidence subjects to qualify): + +- name, description, evidence_subjects (the actual subject lines) +- PROJECTS only: active work the user is building, shipping, or growing. NOT one-time tasks, admin items, legal filings, or calendar events. "Immigration lawyer consultation" is a task, not a project. "Floom" is a project. "Rocketlist" is a project. When in doubt: would this have a GitHub repo or a recurring roadmap? If yes, it's a project. + +**Topics**: recurring themes (5-10 phrases) + +### Stage B: validate locally + +- Drop bots, service accounts, and automated senders (see EXCLUDE list above). +- Keep all named humans with >= 1 real message, even if thin. +- Dedupe entities by email/handle. +- Detect self-addresses (forwarding/cc to alternate emails of the same user) and exclude them from relationships. Self-memos (user emailing themselves) are useful as project/topic evidence only. +- Sort people by evidence weight (Tier A first), keep top 15-20. +- If fewer than 3 people qualify after filtering, lower your standards: include Tier B contacts (single message, clearly human). + +### Stage C: write the profile + +Write THREE files in `~/context/internal/profile/`: + +#### user.md (full profile) + +**IMPORTANT: Use today's actual date for the `generated:` field. Do NOT use a date from training data or memory. If you know the current date, use it. If not, write `generated: [TODAY'S DATE]` as a placeholder.** + +```markdown +--- +generated: YYYY-MM-DD +sources: gmail, linkedin +--- + +## Identity Summary +{2-3 sentences. Plain second person. Cite real projects and current focus.} + +## Key Relationships +| Name | Company | Role | Frequency | Warmth | Context | +|------|---------|------|-----------|--------|---------| +... required: ALWAYS fill role and context with evidence from subjects. + NEVER leave blank. If thin, write the subject pattern as evidence. + +## Recurring Topics +- {topic - how it appears} + +## Active Projects +- **{name}** - {1-line status, cite evidence subjects} + +## Communication Patterns +{3-4 sentences naming top inbound senders, top outbound recipients, dominant +topics, response style. Use real names.} + +## Pending / Watch +- {item - concise present-tense, dedupe near-duplicates} +``` + +#### relationships.md + +Just the Key Relationships table from above. + +#### projects.md + +Just the Active Projects section. + +### Stage D: write context card + +Write `~/context/shareable/profile/context-card.md`: + +```markdown +--- +shareable: true +generated: YYYY-MM-DD +--- + +# {Name} + +**Building:** {1-line of current primary work with company/project name} +**Background:** {2 sentences on past + scale} +**Currently focused on:** +* {3-5 active threads, one line each} +**Best way to work with them:** {2 sentences inferred from communication style} +``` + +Strict 200-word max. No corporate filler. No "passionate about", no "pioneering". Plain second person. The user's actual voice from their actual data. + +## Changelog + +After writing all four profile files, append one line to `~/context/internal/changes.md` (create the file if it does not exist): +``` +YYYY-MM-DD: populate — source: , relationships, projects +``` + +## Voice rules + +- Plain second person ("you are", "you work on") +- No buzzwords ("synergy", "leveraging", "passionate") +- No em dashes +- Cite real names, real subjects, real projects +- If uncertain, say so in [brackets] rather than fabricating +- Always ground claims in evidence + +## Refresh logic + +If `internal/profile/user.md` exists: + +- Check `generated:` date in frontmatter. +- If <7 days old: skip unless the user explicitly asks. +- If 7-30 days old: ask the user "your profile is N days old, refresh?" +- If >30 days old: refresh automatically. + +## Saving facts that surface during synthesis + +If during synthesis you encounter a durable fact that doesn't belong in the +profile (e.g. the user mentions their company's Delaware registration, an +attorney contact, a funding round closing date), use the `fbrain-write-fact` +skill to route it to the correct vault location. Do not dump it into the +profile files or a log file. + +## Output + +After writing, summarize for the user: + +- Source used (Gmail / LinkedIn / described) +- Number of relationships, projects, topics extracted +- File paths written +- Suggested next: "Open a new Claude Code session and ask me what I know about you." + +## Injecting into CLAUDE.md + +After running this skill, paste the following block into your CLAUDE.md to enable brain-first lookups: + +``` +## fbrain: brain-first lookup +Before searching the web or calling external APIs for information about a person, company, or project, check ~/context/internal/profile/relationships.md and ~/context/internal/profile/projects.md first. If a relevant entry exists, use it as the primary source and supplement from external sources only if the entry is stale or incomplete. +``` diff --git a/skills/fbrain-refresh-profile/SKILL.md b/skills/fbrain-refresh-profile/SKILL.md new file mode 100644 index 0000000..7a27fd6 --- /dev/null +++ b/skills/fbrain-refresh-profile/SKILL.md @@ -0,0 +1,92 @@ +--- +name: fbrain-refresh-profile +description: > + Update an existing fbrain profile when it has gone stale. Use when the user + asks to "refresh my profile", "update my context", "re-scan my email", or + when internal/profile/user.md exists but the `generated:` date is older than + 7 days. Delegates to fbrain-populate-profile but only writes if data has + meaningfully changed. +--- + +# fbrain: Refresh Profile + +Update the user's profile in `~/context/internal/profile/` when it has gone stale. This is a delta-aware wrapper around `fbrain-populate-profile`. + +## When to use + +Trigger this skill when: + +- The user explicitly asks to refresh, update, re-scan, or sync their profile. +- A previous session detected the profile is older than 7 days. +- The user mentions a major life or work change ("I left X", "I'm now working on Y") that may have invalidated stored relationships and projects. + +## Pre-flight checks + +1. Read `~/context/internal/profile/user.md`. If it does not exist, route to `fbrain-populate-profile` instead. +2. Parse the `generated:` field from the frontmatter. +3. Compute age: + - <7 days: ask the user "your profile was refreshed N days ago, force a refresh anyway?". If no, stop. + - 7-30 days: proceed without asking. + - >30 days: proceed and note the staleness in your final summary. +4. Read `~/context/internal/profile/relationships.md` and `projects.md` to capture the prior state. + +## Refresh procedure + +Follow the cascade from `fbrain-populate-profile`: + +1. Gmail MCP (preferred). +2. LinkedIn (browser or PDF). +3. User-described (only if both fail). + +Do NOT mix sources. Pick the highest-quality available source and rebuild from it. The point of refresh is to capture changes, not to merge stale data. + +### Delta semantics + +After running Stage A and Stage B from `fbrain-populate-profile`: + +- Compare the new candidate set vs the prior `relationships.md` and `projects.md`. +- If the diff is trivial (no new people, no new projects, no removed people, no new topics): skip the write and tell the user "no meaningful changes since {prior_date}". +- If there is meaningful change: write all four files (user.md, relationships.md, projects.md, context-card.md) using Stage C and Stage D from `fbrain-populate-profile`. + +A "meaningful change" is any of: + +- A new person enters the top 15 relationships. +- A person drops out of the top 15 (e.g. you stopped emailing them). +- A new project appears with >= 2 evidence subjects. +- A previously-active project loses all evidence (status: archived). +- The dominant topics shift by more than 2 entries. + +### Frontmatter + +When writing, update the frontmatter: + +```yaml +--- +generated: YYYY-MM-DD +sources: gmail, linkedin +previous_generated: {prior_date_from_old_file} +--- +``` + +This gives future refresh runs a trail of when the profile last meaningfully changed. + +## Changelog + +After a write (skip if no meaningful changes), append one line to `~/context/internal/changes.md`: +``` +YYYY-MM-DD: refresh — , source: +``` + +## Output + +Summarize for the user: + +- Source used. +- Whether there were meaningful changes (yes/no). +- If yes: bullet list of what changed (new people, removed people, new projects, archived projects). +- If no: confirm the profile is still accurate as of {prior_date}. +- File paths written (or "no files written" if skipped). + +## Voice rules + +Inherit from `fbrain-populate-profile`. Plain second person, no buzzwords, no em dashes, evidence-grounded. diff --git a/skills/fbrain-share-card/SKILL.md b/skills/fbrain-share-card/SKILL.md new file mode 100644 index 0000000..d1eaa25 --- /dev/null +++ b/skills/fbrain-share-card/SKILL.md @@ -0,0 +1,69 @@ +--- +name: fbrain-share-card +description: > + Regenerate the user's shareable context card from existing profile data. Use + when the user asks to "share my context", "generate a context card", "make + a one-pager about me", or when they want a sendable summary for a + collaborator. No external data fetch. Reads ~/context/internal/profile/ and + writes ~/context/shareable/profile/context-card.md. +--- + +# fbrain: Share Card + +Generate a public-safe one-page context card from the user's existing profile. This skill never fetches external data. It only synthesizes from what is already in `~/context/internal/profile/`. + +## When to use + +- The user wants something they can paste into an email, Slack, or a new AI session. +- The user wants to update the card without re-scanning Gmail. +- The user is onboarding a new collaborator and wants a brief intro. + +## Pre-flight checks + +1. Read `~/context/internal/profile/user.md`. If missing, route to `fbrain-populate-profile`. Do NOT fabricate the card. +2. Read `~/context/internal/profile/projects.md` and `relationships.md` for additional grounding. + +## Output spec + +Write `~/context/shareable/profile/context-card.md`: + +```markdown +--- +shareable: true +generated: YYYY-MM-DD +source: internal/profile/user.md +--- + +# {Name} + +**Building:** {1-line of current primary work with company/project name} +**Background:** {2 sentences on past + scale (companies, ARR, team size, education only if relevant)} +**Currently focused on:** +* {3-5 active threads from projects.md, one line each} +**Best way to work with them:** {2 sentences inferred from communication style in user.md} +``` + +## Hard rules + +- **200-word max.** Count words. Trim if over. +- **No corporate filler.** Banned phrases: "passionate about", "pioneering", "leveraging", "synergy", "thought leader", "proven track record", "results-driven", "innovative". +- **No em dashes.** Use commas, semicolons, colons. +- **Plain second person** ("you are working on", "you previously built"). +- **Evidence-grounded.** Every claim must be traceable to user.md, projects.md, or relationships.md. If you cannot find evidence, leave it out rather than invent. +- **No private relationship details.** The card is shareable. Don't list specific contacts by name unless they are public collaborators (co-founders, public team members). Aggregate ("frequent collaborator on X") is fine. +- **No email addresses, no phone numbers, no home addresses.** + +## Voice calibration + +The card sounds like the user, not like a recruiter. If `user.md` shows the user writes short, direct sentences, the card writes short, direct sentences. If `user.md` shows technical language, the card uses technical language. + +Read 2-3 samples from `user.md` Communication Patterns section before drafting to calibrate. + +## Output + +After writing, summarize for the user: + +- File path written. +- Word count. +- Suggested uses: "paste into a new AI session, email a collaborator, or drop into a shared vault". +- Offer: "want to tweak any line?" diff --git a/skills/fbrain-write-fact/SKILL.md b/skills/fbrain-write-fact/SKILL.md new file mode 100644 index 0000000..94782cd --- /dev/null +++ b/skills/fbrain-write-fact/SKILL.md @@ -0,0 +1,144 @@ +--- +name: fbrain-write-fact +description: > + Decide WHERE to write a fact, document, or piece of context in the fbrain + vault. Use whenever you need to persist a fact (legal entity info, project + status, contact details, decision rationale) that doesn't fit neatly into + the synthesized profile. Prevents agents from dumping everything into + log files. + Triggers: "save to vault", "store this in fbrain", "persist this", "add to + my context", "remember this", "write this down", "log this". +--- + +# fbrain: Write a Fact to the Vault + +When asked to save something to the vault, use this decision tree FIRST. Do NOT default to log files. + +## Decision tree + +### Is it a legal/entity/compliance fact? + +Examples: company registration, EIN, VAT ID, articles of incorporation, jurisdiction, registered agent, attorney contact, contracts, NDAs, cap table entries. + +Route to: `vault/legal/.md` (e.g. `vault/legal/floom-inc.md`, `vault/legal/scaile-gmbh.md`) + +- For ongoing matters or disputes: `vault/legal/incidents//` +- For contracts with a specific party: `vault/legal/contracts/.md` + +### Is it about a specific project? + +Examples: launch plans, user research, technical decisions, project status, KPIs, GTM specifics, workplans, feature flags, error logs tied to a project. + +Route to: `vault/projects//.md` + +- For sub-streams: `vault/projects///.md` +- Known project slugs: `floom`, `rocketlist`, `openpaper`, `signaldash`, `cheers`, `agora`, `hyperniche` + +### Is it about a person on your team or in your network? + +Examples: a team member's role and scope, an investor's interest area, a recurring collaborator's background, hiring notes. + +Route to: + +- Your own team: `vault/team//.md` (e.g. `vault/team/floom/cedrik.md`) +- External relationships you actively track: `vault/network/.md` + +### Is it strategy or roadmap? + +Examples: 90-day plan, market positioning, fundraising strategy, OKRs, go-to-market thesis, competitive analysis. + +Route to: `vault/strategy/.md` or `vault/strategy//.md` + +### Is it a one-off note or in-progress thought? + +Examples: "I should remember to ask X about Y", "interesting thing I noticed", informal to-do. + +Route to: `vault/notes/.md` or `internal/scratch/.md` + +Do NOT put this in logs. Logs are time-stamped activity records. + +### Is it secretarial activity? + +Examples: "Sent legal letter 16 to Finom on 2026-04-30", "Meeting with Garima at Founders Inc 2026-05-02", "Completed task X". + +Route to: `vault/secretary/logs/YYYY-MM.md` (one file per month, append to it) + +This is the ONLY category that belongs in log files. + +### Is it credentials or secrets? + +Examples: API keys, OAuth tokens, app passwords, certificates. + +Do NOT write to vault. Use OS keychain (`keyring` library) or password manager. If absolutely necessary and the vault has git-crypt active: `vault/credentials/.md` with a clear warning header. + +### Does it fit an existing top-level vault directory? + +Before creating a new top-level directory, check whether content fits one of the established categories: + +- `vault/applications/` — job or program applications +- `vault/brand/` — logos, brand assets, visual guidelines +- `vault/content/` — LinkedIn posts, blog drafts, social copy, transcripts +- `vault/cv/` — CV and resume versions +- `vault/documents/` — official documents, certificates, diplomas +- `vault/infra/` — server setup, config, tools, scripts +- `vault/legal/` — legal entities, contracts, incidents +- `vault/partnerships/` — active partnership threads +- `vault/pitches/` — investor decks, pitch materials +- `vault/projects/` — active or past product projects +- `vault/research/` — market research, user research, analysis +- `vault/secretary/` — admin, logs, state, scheduling +- `vault/skills/` — custom agent skills +- `vault/strategy/` — plans, positioning, OKRs +- `vault/taxes/` — tax filings and records by entity +- `vault/team/` — team members by entity +- `vault/travel/` — travel plans, attachments + +If none fit: pick the closest, add a sub-directory, and do not invent a new top-level category without asking. + +## How to actually save + +After picking the target path: + +1. **Check if the file already exists.** If yes, append a new section (with a date or topic header) instead of overwriting. + +2. **Use appropriate structure.** For `vault/legal/.md`: + + ```markdown + # + + ## Registration + - **Type**: + - **Jurisdiction**: + - **Incorporated**: + - **EIN / VAT / Tax ID**: + - **Registered agent**: + + ## Officers / Directors + ... + + ## Founding documents + ... + ``` + +3. **Commit with a descriptive message:** + + ```bash + git add vault/legal/floom-inc.md + git commit -m "vault: add Floom Inc Delaware registration" + ``` + + git-crypt encrypts on stage automatically if the machine has git-crypt configured. + +4. **NEVER append legal/entity/durable facts to log files.** Logs are time-stamped activity. Facts are searchable references that must stay in the correct category directory. + +## Anti-patterns + +- Storing company registration in `vault/secretary/logs/icontext.md` — this is the bug this skill exists to prevent +- Storing meeting notes in `vault/legal/` +- Storing contact details for a team member in `vault/strategy/` +- Creating a new top-level vault directory when an existing one fits +- Overwriting an existing file instead of appending a new section + +## When uncertain + +If a fact doesn't fit any category cleanly, ask the user where they want it before guessing. One specific question is cheaper than burying a durable fact in a log the user has to dig out later. diff --git a/skills/icontext-populate-profile/SKILL.md b/skills/icontext-populate-profile/SKILL.md index 3a364a9..10815b6 100644 --- a/skills/icontext-populate-profile/SKILL.md +++ b/skills/icontext-populate-profile/SKILL.md @@ -1,197 +1,6 @@ --- name: icontext-populate-profile -description: > - Build the user's iContext profile from real data sources. Use when the user - asks to "populate my profile", "build my context", "set up icontext", or when - internal/profile/user.md is missing. Cascades through Gmail MCP -> browser - scrape -> PDF -> user-described, picking the highest-quality source available. +description: Deprecated alias for fbrain-populate-profile. Use fbrain-populate-profile instead. Will be removed in v0.6.0. --- -# iContext: Populate Profile - -Build a structured user profile in `~/context/internal/profile/` from real-world data sources. Follow the cascade in order. Use the highest-quality source available. - -## Source cascade (try in order) - -### 1. Gmail (highest quality) - -If a Gmail MCP server is available (e.g. `mcp__gmail__*` or `mcp__claude_ai_Gmail__*` tools), use it: - -- Fetch last 90 days of sent + inbox metadata (subject, from, to, date, cc). -- DO NOT fetch message bodies. Headers only. -- Run the synthesis pipeline in the next section. - -### 2. LinkedIn (if available) - -If browser automation is available (Playwright MCP, claude-in-chrome, etc.): - -- Navigate to the user's LinkedIn profile (ask for the handle if you do not know it). -- Capture work history, education, skills, headline. - -If browser automation is not available: - -- Ask the user to "Save to PDF" their LinkedIn profile (linkedin.com/in/them -> More -> Save to PDF). -- Read the PDF text via the `pdf` skill or pypdf. - -### 3. User-described (lowest friction, lowest quality) - -If neither of the above works, ask the user 4 questions: - -- What are you working on right now? -- What companies/projects matter to you? -- Who do you collaborate with most? -- What is your background in 2-3 sentences? - -## Synthesis rules (apply to whatever data you collected) - -### Stage A: extract structured entities - -From the data, extract: - -**People** (only if there is evidence of a real human relationship): - -- name, email/handle, company, role -- evidence_messages: count of real messages (sent + received combined). Minimum threshold: >= 1 message involving a real named person (not a bot or service). Tier A (hot/warm) = 3+ messages or bidirectional contact. Tier B (cold) = 1-2 messages but clearly human and named. -- direction: mostly_inbound | mostly_outbound | balanced -- topics: 2-3 subject patterns - -**EXCLUDE these as people:** - -- SaaS welcome emails (welcome@, no-reply@, noreply@, team@ from a SaaS domain you only see once) -- Notification senders (notifications@, alerts@, support@, github-actions[bot], dependabot[bot]) -- Billing/payment processors (payments-noreply@, failed-payments@) unless they appear in active human threads -- Automated systems (Cal.com booking confirmations, npm publish notifications, Vercel deploy alerts) -- Anyone where the sender is clearly a bot, automated pipeline, or service account - -**Projects** (need >= 2 evidence subjects to qualify): - -- name, description, evidence_subjects (the actual subject lines) -- PROJECTS only: active work the user is building, shipping, or growing. NOT one-time tasks, admin items, legal filings, or calendar events. "Immigration lawyer consultation" is a task, not a project. "Floom" is a project. "Rocketlist" is a project. When in doubt: would this have a GitHub repo or a recurring roadmap? If yes, it's a project. - -**Topics**: recurring themes (5-10 phrases) - -### Stage B: validate locally - -- Drop bots, service accounts, and automated senders (see EXCLUDE list above). -- Keep all named humans with >= 1 real message, even if thin. -- Dedupe entities by email/handle. -- Detect self-addresses (forwarding/cc to alternate emails of the same user) and exclude them from relationships. Self-memos (user emailing themselves) are useful as project/topic evidence only. -- Sort people by evidence weight (Tier A first), keep top 15-20. -- If fewer than 3 people qualify after filtering, lower your standards: include Tier B contacts (single message, clearly human). - -### Stage C: write the profile - -Write THREE files in `~/context/internal/profile/`: - -#### user.md (full profile) - -**IMPORTANT: Use today's actual date for the `generated:` field. Do NOT use a date from training data or memory. If you know the current date, use it. If not, write `generated: [TODAY'S DATE]` as a placeholder.** - -```markdown ---- -generated: YYYY-MM-DD -sources: gmail, linkedin ---- - -## Identity Summary -{2-3 sentences. Plain second person. Cite real projects and current focus.} - -## Key Relationships -| Name | Company | Role | Frequency | Warmth | Context | -|------|---------|------|-----------|--------|---------| -... required: ALWAYS fill role and context with evidence from subjects. - NEVER leave blank. If thin, write the subject pattern as evidence. - -## Recurring Topics -- {topic - how it appears} - -## Active Projects -- **{name}** - {1-line status, cite evidence subjects} - -## Communication Patterns -{3-4 sentences naming top inbound senders, top outbound recipients, dominant -topics, response style. Use real names.} - -## Pending / Watch -- {item - concise present-tense, dedupe near-duplicates} -``` - -#### relationships.md - -Just the Key Relationships table from above. - -#### projects.md - -Just the Active Projects section. - -### Stage D: write context card - -Write `~/context/shareable/profile/context-card.md`: - -```markdown ---- -shareable: true -generated: YYYY-MM-DD ---- - -# {Name} - -**Building:** {1-line of current primary work with company/project name} -**Background:** {2 sentences on past + scale} -**Currently focused on:** -* {3-5 active threads, one line each} -**Best way to work with them:** {2 sentences inferred from communication style} -``` - -Strict 200-word max. No corporate filler. No "passionate about", no "pioneering". Plain second person. The user's actual voice from their actual data. - -## Changelog - -After writing all four profile files, append one line to `~/context/internal/changes.md` (create the file if it does not exist): -``` -YYYY-MM-DD: populate — source: , relationships, projects -``` - -## Voice rules - -- Plain second person ("you are", "you work on") -- No buzzwords ("synergy", "leveraging", "passionate") -- No em dashes -- Cite real names, real subjects, real projects -- If uncertain, say so in [brackets] rather than fabricating -- Always ground claims in evidence - -## Refresh logic - -If `internal/profile/user.md` exists: - -- Check `generated:` date in frontmatter. -- If <7 days old: skip unless the user explicitly asks. -- If 7-30 days old: ask the user "your profile is N days old, refresh?" -- If >30 days old: refresh automatically. - -## Saving facts that surface during synthesis - -If during synthesis you encounter a durable fact that doesn't belong in the -profile (e.g. the user mentions their company's Delaware registration, an -attorney contact, a funding round closing date), use the `icontext-write-fact` -skill to route it to the correct vault location. Do not dump it into the -profile files or a log file. - -## Output - -After writing, summarize for the user: - -- Source used (Gmail / LinkedIn / described) -- Number of relationships, projects, topics extracted -- File paths written -- Suggested next: "Open a new Claude Code session and ask me what I know about you." - -## Injecting into CLAUDE.md - -After running this skill, paste the following block into your CLAUDE.md to enable brain-first lookups: - -``` -## fbrain: brain-first lookup -Before searching the web or calling external APIs for information about a person, company, or project, check ~/context/internal/profile/relationships.md and ~/context/internal/profile/projects.md first. If a relevant entry exists, use it as the primary source and supplement from external sources only if the entry is stale or incomplete. -``` +This skill has been renamed. See skills/fbrain-populate-profile/SKILL.md for the canonical version. diff --git a/skills/icontext-refresh-profile/SKILL.md b/skills/icontext-refresh-profile/SKILL.md index 5ca0cc1..b52d9fa 100644 --- a/skills/icontext-refresh-profile/SKILL.md +++ b/skills/icontext-refresh-profile/SKILL.md @@ -1,92 +1,6 @@ --- name: icontext-refresh-profile -description: > - Update an existing iContext profile when it has gone stale. Use when the user - asks to "refresh my profile", "update my context", "re-scan my email", or - when internal/profile/user.md exists but the `generated:` date is older than - 7 days. Delegates to icontext-populate-profile but only writes if data has - meaningfully changed. +description: Deprecated alias for fbrain-refresh-profile. Use fbrain-refresh-profile instead. Will be removed in v0.6.0. --- -# iContext: Refresh Profile - -Update the user's profile in `~/context/internal/profile/` when it has gone stale. This is a delta-aware wrapper around `icontext-populate-profile`. - -## When to use - -Trigger this skill when: - -- The user explicitly asks to refresh, update, re-scan, or sync their profile. -- A previous session detected the profile is older than 7 days. -- The user mentions a major life or work change ("I left X", "I'm now working on Y") that may have invalidated stored relationships and projects. - -## Pre-flight checks - -1. Read `~/context/internal/profile/user.md`. If it does not exist, route to `icontext-populate-profile` instead. -2. Parse the `generated:` field from the frontmatter. -3. Compute age: - - <7 days: ask the user "your profile was refreshed N days ago, force a refresh anyway?". If no, stop. - - 7-30 days: proceed without asking. - - >30 days: proceed and note the staleness in your final summary. -4. Read `~/context/internal/profile/relationships.md` and `projects.md` to capture the prior state. - -## Refresh procedure - -Follow the cascade from `icontext-populate-profile`: - -1. Gmail MCP (preferred). -2. LinkedIn (browser or PDF). -3. User-described (only if both fail). - -Do NOT mix sources. Pick the highest-quality available source and rebuild from it. The point of refresh is to capture changes, not to merge stale data. - -### Delta semantics - -After running Stage A and Stage B from `icontext-populate-profile`: - -- Compare the new candidate set vs the prior `relationships.md` and `projects.md`. -- If the diff is trivial (no new people, no new projects, no removed people, no new topics): skip the write and tell the user "no meaningful changes since {prior_date}". -- If there is meaningful change: write all four files (user.md, relationships.md, projects.md, context-card.md) using Stage C and Stage D from `icontext-populate-profile`. - -A "meaningful change" is any of: - -- A new person enters the top 15 relationships. -- A person drops out of the top 15 (e.g. you stopped emailing them). -- A new project appears with >= 2 evidence subjects. -- A previously-active project loses all evidence (status: archived). -- The dominant topics shift by more than 2 entries. - -### Frontmatter - -When writing, update the frontmatter: - -```yaml ---- -generated: YYYY-MM-DD -sources: gmail, linkedin -previous_generated: {prior_date_from_old_file} ---- -``` - -This gives future refresh runs a trail of when the profile last meaningfully changed. - -## Changelog - -After a write (skip if no meaningful changes), append one line to `~/context/internal/changes.md`: -``` -YYYY-MM-DD: refresh — , source: -``` - -## Output - -Summarize for the user: - -- Source used. -- Whether there were meaningful changes (yes/no). -- If yes: bullet list of what changed (new people, removed people, new projects, archived projects). -- If no: confirm the profile is still accurate as of {prior_date}. -- File paths written (or "no files written" if skipped). - -## Voice rules - -Inherit from `icontext-populate-profile`. Plain second person, no buzzwords, no em dashes, evidence-grounded. +This skill has been renamed. See skills/fbrain-refresh-profile/SKILL.md for the canonical version. diff --git a/skills/icontext-share-card/SKILL.md b/skills/icontext-share-card/SKILL.md index e73092b..1c3656e 100644 --- a/skills/icontext-share-card/SKILL.md +++ b/skills/icontext-share-card/SKILL.md @@ -1,69 +1,6 @@ --- name: icontext-share-card -description: > - Regenerate the user's shareable context card from existing profile data. Use - when the user asks to "share my context", "generate a context card", "make - a one-pager about me", or when they want a sendable summary for a - collaborator. No external data fetch. Reads ~/context/internal/profile/ and - writes ~/context/shareable/profile/context-card.md. +description: Deprecated alias for fbrain-share-card. Use fbrain-share-card instead. Will be removed in v0.6.0. --- -# iContext: Share Card - -Generate a public-safe one-page context card from the user's existing profile. This skill never fetches external data. It only synthesizes from what is already in `~/context/internal/profile/`. - -## When to use - -- The user wants something they can paste into an email, Slack, or a new AI session. -- The user wants to update the card without re-scanning Gmail. -- The user is onboarding a new collaborator and wants a brief intro. - -## Pre-flight checks - -1. Read `~/context/internal/profile/user.md`. If missing, route to `icontext-populate-profile`. Do NOT fabricate the card. -2. Read `~/context/internal/profile/projects.md` and `relationships.md` for additional grounding. - -## Output spec - -Write `~/context/shareable/profile/context-card.md`: - -```markdown ---- -shareable: true -generated: YYYY-MM-DD -source: internal/profile/user.md ---- - -# {Name} - -**Building:** {1-line of current primary work with company/project name} -**Background:** {2 sentences on past + scale (companies, ARR, team size, education only if relevant)} -**Currently focused on:** -* {3-5 active threads from projects.md, one line each} -**Best way to work with them:** {2 sentences inferred from communication style in user.md} -``` - -## Hard rules - -- **200-word max.** Count words. Trim if over. -- **No corporate filler.** Banned phrases: "passionate about", "pioneering", "leveraging", "synergy", "thought leader", "proven track record", "results-driven", "innovative". -- **No em dashes.** Use commas, semicolons, colons. -- **Plain second person** ("you are working on", "you previously built"). -- **Evidence-grounded.** Every claim must be traceable to user.md, projects.md, or relationships.md. If you cannot find evidence, leave it out rather than invent. -- **No private relationship details.** The card is shareable. Don't list specific contacts by name unless they are public collaborators (co-founders, public team members). Aggregate ("frequent collaborator on X") is fine. -- **No email addresses, no phone numbers, no home addresses.** - -## Voice calibration - -The card sounds like the user, not like a recruiter. If `user.md` shows the user writes short, direct sentences, the card writes short, direct sentences. If `user.md` shows technical language, the card uses technical language. - -Read 2-3 samples from `user.md` Communication Patterns section before drafting to calibrate. - -## Output - -After writing, summarize for the user: - -- File path written. -- Word count. -- Suggested uses: "paste into a new AI session, email a collaborator, or drop into a shared vault". -- Offer: "want to tweak any line?" +This skill has been renamed. See skills/fbrain-share-card/SKILL.md for the canonical version. diff --git a/skills/icontext-write-fact/SKILL.md b/skills/icontext-write-fact/SKILL.md index 2c63ae6..5a5643a 100644 --- a/skills/icontext-write-fact/SKILL.md +++ b/skills/icontext-write-fact/SKILL.md @@ -1,144 +1,6 @@ --- name: icontext-write-fact -description: > - Decide WHERE to write a fact, document, or piece of context in the iContext - vault. Use whenever you need to persist a fact (legal entity info, project - status, contact details, decision rationale) that doesn't fit neatly into - the synthesized profile. Prevents agents from dumping everything into - log files. - Triggers: "save to vault", "store this in icontext", "persist this", "add to - my context", "remember this", "write this down", "log this". +description: Deprecated alias for fbrain-write-fact. Use fbrain-write-fact instead. Will be removed in v0.6.0. --- -# iContext: Write a Fact to the Vault - -When asked to save something to the vault, use this decision tree FIRST. Do NOT default to log files. - -## Decision tree - -### Is it a legal/entity/compliance fact? - -Examples: company registration, EIN, VAT ID, articles of incorporation, jurisdiction, registered agent, attorney contact, contracts, NDAs, cap table entries. - -Route to: `vault/legal/.md` (e.g. `vault/legal/floom-inc.md`, `vault/legal/scaile-gmbh.md`) - -- For ongoing matters or disputes: `vault/legal/incidents//` -- For contracts with a specific party: `vault/legal/contracts/.md` - -### Is it about a specific project? - -Examples: launch plans, user research, technical decisions, project status, KPIs, GTM specifics, workplans, feature flags, error logs tied to a project. - -Route to: `vault/projects//.md` - -- For sub-streams: `vault/projects///.md` -- Known project slugs: `floom`, `rocketlist`, `openpaper`, `signaldash`, `cheers`, `agora`, `hyperniche` - -### Is it about a person on your team or in your network? - -Examples: a team member's role and scope, an investor's interest area, a recurring collaborator's background, hiring notes. - -Route to: - -- Your own team: `vault/team//.md` (e.g. `vault/team/floom/cedrik.md`) -- External relationships you actively track: `vault/network/.md` - -### Is it strategy or roadmap? - -Examples: 90-day plan, market positioning, fundraising strategy, OKRs, go-to-market thesis, competitive analysis. - -Route to: `vault/strategy/.md` or `vault/strategy//.md` - -### Is it a one-off note or in-progress thought? - -Examples: "I should remember to ask X about Y", "interesting thing I noticed", informal to-do. - -Route to: `vault/notes/.md` or `internal/scratch/.md` - -Do NOT put this in logs. Logs are time-stamped activity records. - -### Is it secretarial activity? - -Examples: "Sent legal letter 16 to Finom on 2026-04-30", "Meeting with Garima at Founders Inc 2026-05-02", "Completed task X". - -Route to: `vault/secretary/logs/YYYY-MM.md` (one file per month, append to it) - -This is the ONLY category that belongs in log files. - -### Is it credentials or secrets? - -Examples: API keys, OAuth tokens, app passwords, certificates. - -Do NOT write to vault. Use OS keychain (`keyring` library) or password manager. If absolutely necessary and the vault has git-crypt active: `vault/credentials/.md` with a clear warning header. - -### Does it fit an existing top-level vault directory? - -Before creating a new top-level directory, check whether content fits one of the established categories: - -- `vault/applications/` — job or program applications -- `vault/brand/` — logos, brand assets, visual guidelines -- `vault/content/` — LinkedIn posts, blog drafts, social copy, transcripts -- `vault/cv/` — CV and resume versions -- `vault/documents/` — official documents, certificates, diplomas -- `vault/infra/` — server setup, config, tools, scripts -- `vault/legal/` — legal entities, contracts, incidents -- `vault/partnerships/` — active partnership threads -- `vault/pitches/` — investor decks, pitch materials -- `vault/projects/` — active or past product projects -- `vault/research/` — market research, user research, analysis -- `vault/secretary/` — admin, logs, state, scheduling -- `vault/skills/` — custom agent skills -- `vault/strategy/` — plans, positioning, OKRs -- `vault/taxes/` — tax filings and records by entity -- `vault/team/` — team members by entity -- `vault/travel/` — travel plans, attachments - -If none fit: pick the closest, add a sub-directory, and do not invent a new top-level category without asking. - -## How to actually save - -After picking the target path: - -1. **Check if the file already exists.** If yes, append a new section (with a date or topic header) instead of overwriting. - -2. **Use appropriate structure.** For `vault/legal/.md`: - - ```markdown - # - - ## Registration - - **Type**: - - **Jurisdiction**: - - **Incorporated**: - - **EIN / VAT / Tax ID**: - - **Registered agent**: - - ## Officers / Directors - ... - - ## Founding documents - ... - ``` - -3. **Commit with a descriptive message:** - - ```bash - git add vault/legal/floom-inc.md - git commit -m "vault: add Floom Inc Delaware registration" - ``` - - git-crypt encrypts on stage automatically if the machine has git-crypt configured. - -4. **NEVER append legal/entity/durable facts to log files.** Logs are time-stamped activity. Facts are searchable references that must stay in the correct category directory. - -## Anti-patterns - -- Storing company registration in `vault/secretary/logs/icontext.md` — this is the bug this skill exists to prevent -- Storing meeting notes in `vault/legal/` -- Storing contact details for a team member in `vault/strategy/` -- Creating a new top-level vault directory when an existing one fits -- Overwriting an existing file instead of appending a new section - -## When uncertain - -If a fact doesn't fit any category cleanly, ask the user where they want it before guessing. One specific question is cheaper than burying a durable fact in a log the user has to dig out later. +This skill has been renamed. See skills/fbrain-write-fact/SKILL.md for the canonical version. diff --git a/tests/test_cli.py b/tests/test_cli.py index 9c1091a..ab46937 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -21,12 +21,13 @@ def _safe_env(home: str | None = None) -> dict: env = os.environ.copy() if home is not None: env["HOME"] = home + env.pop("FBRAIN_VAULT", None) env.pop("ICONTEXT_VAULT", None) return env def run(*args: str, vault: str | None = None, env: dict | None = None) -> subprocess.CompletedProcess: - """Run icontext CLI. + """Run fbrain CLI. For subcommands (init, status, sync, share, doctor): `args[0]` is the subcommand and `--vault` is injected right after it (each subparser registers --vault locally). @@ -62,13 +63,13 @@ def test_version_exits_0_and_prints_version(self): # argparse sends --version to stdout on some platforms, stderr on others output = result.stdout + result.stderr self.assertEqual(result.returncode, 0) - self.assertRegex(output, r"icontext \d+\.\d+\.\d+") + self.assertRegex(output, r"fbrain \d+\.\d+\.\d+") def test_no_args_exits_1_and_prints_help(self): result = run() self.assertEqual(result.returncode, 1) output = result.stdout + result.stderr - self.assertIn("icontext", output.lower()) + self.assertIn("fbrain", output.lower()) # --------------------------------------------------------------------------- @@ -127,7 +128,7 @@ def test_init_creates_git_repo(self): run("init", vault=str(vault), env=_safe_env(str(home))) self.assertTrue( (vault / ".git").exists(), - "Expected .git directory after icontext init" + "Expected .git directory after fbrain init" ) def test_init_installs_skill_files(self): @@ -139,7 +140,7 @@ def test_init_installs_skill_files(self): result = run("init", vault=str(vault), env=_safe_env(str(home))) output = result.stdout + result.stderr skills_dir = home / ".claude" / "skills" - for name in ("icontext-populate-profile", "icontext-refresh-profile", "icontext-share-card"): + for name in ("fbrain-populate-profile", "fbrain-refresh-profile", "fbrain-share-card"): skill = skills_dir / name / "SKILL.md" self.assertTrue( skill.exists(), @@ -157,7 +158,7 @@ def test_init_installs_cursor_rules(self): vault = Path(tmp) / "cursor-vault" run("init", vault=str(vault), env=_safe_env(str(home))) rules_dir = home / ".cursor" / "rules" - for name in ("icontext-populate-profile", "icontext-refresh-profile", "icontext-share-card"): + for name in ("fbrain-populate-profile", "fbrain-refresh-profile", "fbrain-share-card"): self.assertTrue((rules_dir / f"{name}.mdc").exists(), f"Missing cursor rule {name}.mdc") @@ -170,10 +171,10 @@ def test_init_writes_claude_md_snippet_referencing_skills(self): claude_md = home / ".claude" / "CLAUDE.md" self.assertTrue(claude_md.exists(), "CLAUDE.md not created") text = claude_md.read_text() - self.assertIn("", text) - self.assertIn("icontext-populate-profile", text) - self.assertIn("icontext-refresh-profile", text) - self.assertIn("icontext-share-card", text) + self.assertIn("", text) + self.assertIn("fbrain-populate-profile", text) + self.assertIn("fbrain-refresh-profile", text) + self.assertIn("fbrain-share-card", text) self.assertIn("internal/profile/user.md", text) def test_init_does_not_require_gemini_key(self): @@ -250,7 +251,7 @@ def test_skills_list_after_init_shows_three_skills(self): result = run("skills", "list", env=_safe_env(str(home))) self.assertEqual(result.returncode, 0) output = result.stdout + result.stderr - for name in ("icontext-populate-profile", "icontext-refresh-profile", "icontext-share-card"): + for name in ("fbrain-populate-profile", "fbrain-refresh-profile", "fbrain-share-card"): self.assertIn(name, output) def test_skills_no_action_defaults_to_list(self): @@ -262,7 +263,7 @@ def test_skills_no_action_defaults_to_list(self): result = run("skills", env=_safe_env(str(home))) self.assertEqual(result.returncode, 0) output = result.stdout + result.stderr - self.assertIn("icontext-populate-profile", output) + self.assertIn("fbrain-populate-profile", output) # --------------------------------------------------------------------------- diff --git a/tests/test_doctor_fresh_install.py b/tests/test_doctor_fresh_install.py index e44f13a..d3998d9 100644 --- a/tests/test_doctor_fresh_install.py +++ b/tests/test_doctor_fresh_install.py @@ -26,7 +26,7 @@ def write_executable(path: Path, content: str) -> None: def make_icontext_root(tmp_path: Path) -> Path: - root = tmp_path / "icontext" + root = tmp_path / "fbrain" for directory in ["config", "workflows", "mcp", "scripts", "hooks"]: (root / directory).mkdir(parents=True) (root / "config" / "gitleaks.toml").write_text("title = 'test'\n", encoding="utf-8") diff --git a/tests/test_index_mcp.py b/tests/test_index_mcp.py index 646b827..0737aec 100644 --- a/tests/test_index_mcp.py +++ b/tests/test_index_mcp.py @@ -90,7 +90,7 @@ class IcontextIntegrationInstallTests(unittest.TestCase): def test_agent_configs_are_installed_idempotently(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) - icontext_root = root / "icontext" + icontext_root = root / "fbrain" repo = root / "context" claude_dir = root / ".claude" codex_config = root / ".codex" / "config.toml" @@ -116,11 +116,11 @@ def test_agent_configs_are_installed_idempotently(self): cursor = json.loads(cursor_mcp.read_text(encoding="utf-8")) opencode = json.loads(opencode_config.read_text(encoding="utf-8")) - self.assertEqual(claude_mcp["mcpServers"]["icontext"]["command"], "python3") + self.assertEqual(claude_mcp["mcpServers"]["fbrain"]["command"], "python3") self.assertEqual(len(claude_settings["hooks"]["UserPromptSubmit"]), 1) - self.assertEqual(codex["mcp_servers"]["icontext"]["command"], "python3") - self.assertEqual(cursor["mcpServers"]["icontext"]["args"][-1], str(repo)) - self.assertEqual(opencode["mcp"]["icontext"]["command"][0], "python3") + self.assertEqual(codex["mcp_servers"]["fbrain"]["command"], "python3") + self.assertEqual(cursor["mcpServers"]["fbrain"]["args"][-1], str(repo)) + self.assertEqual(opencode["mcp"]["fbrain"]["command"][0], "python3") if __name__ == "__main__": diff --git a/tests/test_skill_routing.py b/tests/test_skill_routing.py index 68aaabd..667c19f 100644 --- a/tests/test_skill_routing.py +++ b/tests/test_skill_routing.py @@ -1,11 +1,11 @@ -"""Tests for the icontext-write-fact skill routing.""" +"""Tests for the fbrain-write-fact skill routing.""" from __future__ import annotations import unittest from pathlib import Path SKILLS_ROOT = Path(__file__).resolve().parents[1] / "skills" -WRITE_FACT_SKILL = SKILLS_ROOT / "icontext-write-fact" / "SKILL.md" +WRITE_FACT_SKILL = SKILLS_ROOT / "fbrain-write-fact" / "SKILL.md" class TestWriteFactSkillExists(unittest.TestCase): @@ -13,7 +13,7 @@ class TestWriteFactSkillExists(unittest.TestCase): def test_skill_file_exists(self): self.assertTrue( WRITE_FACT_SKILL.exists(), - f"icontext-write-fact SKILL.md missing at {WRITE_FACT_SKILL}" + f"fbrain-write-fact SKILL.md missing at {WRITE_FACT_SKILL}" ) def test_skill_has_frontmatter(self): @@ -26,8 +26,8 @@ def test_skill_has_frontmatter(self): def test_skill_frontmatter_has_name(self): content = WRITE_FACT_SKILL.read_text() - self.assertIn("name: icontext-write-fact", content, - "frontmatter must declare name: icontext-write-fact") + self.assertIn("name: fbrain-write-fact", content, + "frontmatter must declare name: fbrain-write-fact") def test_skill_frontmatter_has_description(self): content = WRITE_FACT_SKILL.read_text() @@ -77,7 +77,7 @@ def test_antipattern_log_file_mentioned(self): class TestWriteFactInstalledByInit(unittest.TestCase): - """Verify the skill ships via icontext init.""" + """Verify the skill ships via fbrain init.""" def test_init_installs_write_fact_skill(self): import os @@ -92,15 +92,16 @@ def test_init_installs_write_fact_skill(self): vault = Path(tmp) / "vault" env = os.environ.copy() env["HOME"] = str(home) + env.pop("FBRAIN_VAULT", None) env.pop("ICONTEXT_VAULT", None) subprocess.run( [sys.executable, cli, "init", "--vault", str(vault)], capture_output=True, text=True, env=env, ) - skill_path = home / ".claude" / "skills" / "icontext-write-fact" / "SKILL.md" + skill_path = home / ".claude" / "skills" / "fbrain-write-fact" / "SKILL.md" self.assertTrue( skill_path.exists(), - f"icontext-write-fact not installed by init. Expected at {skill_path}" + f"fbrain-write-fact not installed by init. Expected at {skill_path}" ) def test_init_installs_cursor_rule_for_write_fact(self): @@ -116,15 +117,16 @@ def test_init_installs_cursor_rule_for_write_fact(self): vault = Path(tmp) / "vault" env = os.environ.copy() env["HOME"] = str(home) + env.pop("FBRAIN_VAULT", None) env.pop("ICONTEXT_VAULT", None) subprocess.run( [sys.executable, cli, "init", "--vault", str(vault)], capture_output=True, text=True, env=env, ) - cursor_path = home / ".cursor" / "rules" / "icontext-write-fact.mdc" + cursor_path = home / ".cursor" / "rules" / "fbrain-write-fact.mdc" self.assertTrue( cursor_path.exists(), - f"icontext-write-fact cursor rule not installed. Expected at {cursor_path}" + f"fbrain-write-fact cursor rule not installed. Expected at {cursor_path}" ) def test_skills_list_shows_four_skills(self): @@ -140,6 +142,7 @@ def test_skills_list_shows_four_skills(self): vault = Path(tmp) / "vault" env = os.environ.copy() env["HOME"] = str(home) + env.pop("FBRAIN_VAULT", None) env.pop("ICONTEXT_VAULT", None) subprocess.run( [sys.executable, cli, "init", "--vault", str(vault)], @@ -151,10 +154,10 @@ def test_skills_list_shows_four_skills(self): ) output = result.stdout + result.stderr for name in ( - "icontext-populate-profile", - "icontext-refresh-profile", - "icontext-share-card", - "icontext-write-fact", + "fbrain-populate-profile", + "fbrain-refresh-profile", + "fbrain-share-card", + "fbrain-write-fact", ): self.assertIn(name, output, f"skills list missing {name} after init") @@ -176,6 +179,7 @@ def test_claude_md_snippet_includes_write_fact(self): vault = Path(tmp) / "vault" env = os.environ.copy() env["HOME"] = str(home) + env.pop("FBRAIN_VAULT", None) env.pop("ICONTEXT_VAULT", None) subprocess.run( [sys.executable, cli, "init", "--vault", str(vault)], @@ -184,8 +188,8 @@ def test_claude_md_snippet_includes_write_fact(self): claude_md = home / ".claude" / "CLAUDE.md" self.assertTrue(claude_md.exists(), "CLAUDE.md not written by init") content = claude_md.read_text() - self.assertIn("icontext-write-fact", content, - "CLAUDE.md snippet must reference icontext-write-fact") + self.assertIn("fbrain-write-fact", content, + "CLAUDE.md snippet must reference fbrain-write-fact") if __name__ == "__main__": diff --git a/uninstall.sh b/uninstall.sh index eaf16e6..010d700 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# icontext uninstaller: remove files recorded in .icontext/manifest.json. +# fbrain uninstaller: remove files recorded in .icontext/manifest.json. set -euo pipefail @@ -23,7 +23,7 @@ while [ "$#" -gt 0 ]; do case "$1" in --vault) VAULT="${2:-}" - [ -n "$VAULT" ] || { echo "icontext: --vault requires a path"; exit 1; } + [ -n "$VAULT" ] || { echo "fbrain: --vault requires a path"; exit 1; } shift 2 ;; --dry-run) @@ -39,7 +39,7 @@ while [ "$#" -gt 0 ]; do exit 0 ;; -*) - echo "icontext: unknown option: $1" + echo "fbrain: unknown option: $1" usage exit 1 ;; @@ -53,21 +53,21 @@ done VAULT="$(cd "$VAULT" && pwd)" if [ ! -d "$VAULT/.git" ]; then - echo "icontext: $VAULT is not a git repo" + echo "fbrain: $VAULT is not a git repo" exit 1 fi MANIFEST="$VAULT/.icontext/manifest.json" -echo "icontext: uninstall plan" +echo "fbrain: uninstall plan" echo " target: $VAULT" if [ "$DRY_RUN" -eq 1 ]; then echo " dry-run: yes" fi if [ ! -f "$MANIFEST" ]; then - echo "icontext: manifest missing; refusing manifest-aware uninstall" - echo "icontext: remove legacy installs manually or reinstall icontext to create a manifest" + echo "fbrain: manifest missing; refusing manifest-aware uninstall" + echo "fbrain: remove legacy installs manually or reinstall fbrain to create a manifest" exit 1 fi @@ -153,14 +153,14 @@ PY if [ "$DRY_RUN" -eq 1 ]; then run_uninstall_plan 1 - echo "icontext: dry run complete; no files were removed" + echo "fbrain: dry run complete; no files were removed" exit 0 fi if [ "$YES" -eq 0 ]; then if [ ! -t 0 ]; then echo "" - echo "icontext: refusing to uninstall non-interactively without --yes" + echo "fbrain: refusing to uninstall non-interactively without --yes" exit 1 fi echo "" @@ -169,7 +169,7 @@ if [ "$YES" -eq 0 ]; then case "$answer" in y|Y|yes|YES) ;; *) - echo "icontext: cancelled" + echo "fbrain: cancelled" exit 1 ;; esac @@ -177,4 +177,4 @@ fi run_uninstall_plan 0 -echo "icontext: uninstall complete" +echo "fbrain: uninstall complete" diff --git a/workflows/sensitivity.yml b/workflows/sensitivity.yml index 7ebb9f6..eed64b4 100644 --- a/workflows/sensitivity.yml +++ b/workflows/sensitivity.yml @@ -1,4 +1,4 @@ -name: icontext sensitivity +name: fbrain sensitivity on: push: