CLI & MCP

CLI and MCP server for Cotext preferences. Reads the .cotext folder synced by the browser extension and exposes the active profile's prompt to your terminal and to MCP-aware AI clients.

Install

npm install -g cotext

This installs two binaries: cotext (CLI) and cotext-mcp (stdio MCP server).

CLI

Point the CLI at the folder the browser extension is syncing to:

cotext init                  # interactive — saves to ~/.config/cotext/config.json
cotext status                # source, active profile, last sync
cotext list                  # all profiles (* marks the active one)
cotext pull                  # active prompt to stdout — pipe wherever
cotext pull --profile code-review
cotext cat <slug>            # full markdown including header
cotext signal "be terser" --type dislike --tag too-verbose
cotext profile create "Code review" --starter code-review --activate
cotext daemon                # process the folder's inbox in real time
cotext synth                 # one-shot library+prose synthesis on active profile
cotext mcp-install           # register cotext-mcp with Claude Code / Codex
cotext mcp-prompt            # print the CLAUDE.md snippet for agents

A common pattern is to refresh CLAUDE.md from the active profile:

cotext pull > CLAUDE.md

Recording signals

cotext signal drops a feedback signal into <folder>/inbox/; the browser extension picks it up on next popup open, runs interpretation, and folds the result into the active profile. Signals carry the same shape as browser 👍/👎 reactions — see below for the full flag set.

# Minimal — note only
cotext signal "be more concise" --type dislike

# With a preset tag
cotext signal --type dislike --tag too-verbose

# With conversation context (gives the interpreter something to ground
# the rule against — without this, the note is the only signal)
cotext signal "this format is exactly right" --type like \
  --response "$(cat /tmp/last-claude-output.txt)" \
  --prompt "explain the redux flow"

# Pipe stdin as the AI response — most ergonomic with Claude Code
claude --print "explain X" | cotext signal "good shape" --type like

# Read from files
cotext signal "skip the boilerplate" --type dislike \
  --response-file /tmp/output.txt \
  --prompt-file /tmp/prompt.txt

Full signal flag set:

FlagPurpose
--type <like|dislike|tag>Signal kind (default: tag)
--tag <slug>Preset tag (e.g. too-verbose, preachy)
--profile <slug>Target profile (sets context.contextProfile)
--response <text>The AI response the signal is about
--response-file <path>Read the AI response from a file
--prompt <text>The user prompt that produced the response
--prompt-file <path>Read the user prompt from a file
--provider <name>Provider hint (default: cli)

If --response/--response-file are omitted and stdin isn't a TTY, the CLI reads stdin as the AI response. --type defaults to tag (neutral note); --type like/dislike carry polarity.

Creating profiles

cotext profile create <name> creates a profile against whichever source is configured.

  • Folder source: queues a create-profile command into <folder>/inbox/. The browser extension materializes it on the next popup open — until then it won't appear in cotext list.
  • Cloud source (COTEXT_API_TOKEN): builds the profile from the starter pack locally and POSTs it directly to cotext.io/api/push — visible immediately at https://cotext.io/@<your-username>/<slug>.
# Minimal — new "Code review" profile, default context, no seed
cotext profile create "Code review"

# Seeded from a starter pack + activated immediately
cotext profile create "Code review" --starter code-review --activate

# Free-form context appears in the synthesized prompt header and
# biases LLM passes. Description lands as customInstructions.
cotext profile create "Writing" \
  --context "product copy and internal docs" \
  --description "I write product copy and internal docs"

# Free-form context + explicit /explore category. Useful when the
# context wouldn't map to a curated slug on its own.
cotext profile create "Startup code review" \
  --context "code review at a 50-person startup" \
  --category coding \
  --starter code-review --activate

Full profile create flags:

FlagPurpose
--context <text>Free-form descriptor of what this profile is for. Appears in the synthesized prompt header and biases every LLM pass. e.g. coding, "code review at a 50-person startup". Default: general.
--category <slug>Curated category for the /explore listing on cotext.io. One of: general, coding, writing, research, creative, learning, business, productivity, analysis, support. Defaults to derive from --context (falls back to general if the context is free-form and doesn't match a slug).
--starter <pack>Seed entries + metrics from a starter pack id
--description <text>Free-form note kept verbatim as customInstructions
--activateMake this the active profile after creation

Starter pack ids: code-review, email-writing, research, daily-driver, tutor, patient-explainer, strategic, creative-writing, brainstorm, sprint-planner, data-analyst, support-helper. Run cotext list after opening the popup to see how they materialize.

Daemon — Cotext without the browser extension

cotext daemon is a long-running process that drains the folder's inbox/ in real time. Use it when:

  • You don't have (or don't want) the browser extension installed
  • You want CLI signals to be interpreted within seconds, not on the next popup open
  • You're running Cotext on a server / headless box

The daemon supports three providers: Ollama (default), Anthropic Claude, and OpenAI. Pick whichever fits your environment.

# Default — Ollama running locally
ollama serve &
ollama pull qwen3.5:4b
cotext daemon

# Anthropic Claude — no Ollama install needed
export COTEXT_ANTHROPIC_KEY=sk-ant-...
cotext daemon --provider anthropic

# OpenAI
export COTEXT_OPENAI_KEY=sk-...
cotext daemon --provider openai

# Override endpoint / model / poll interval (Ollama)
cotext daemon --endpoint http://192.168.1.5:11434 --model llama3.2:3b --poll 1000

# Override cloud-provider model
cotext daemon --provider anthropic --anthropic-model claude-sonnet-4-6

When --provider isn't given, the daemon picks one based on which keys are set: Anthropic key → Anthropic; else OpenAI key → OpenAI; else Ollama. Easy to flip between providers via env vars without touching the command line.

What the daemon does, on every poll (every 1.5s by default):

  1. Reads <folder>/inbox/*.json
  2. For each signal file: reads the active profile's context and interpretationHint from disk, calls Ollama to interpret with both injected into the prompt, appends a new PreferenceEntry to the active profile, regenerates <slug>.json + <slug>.md, appends to signals.jsonl, deletes the inbox file
  3. For each command file (e.g. cmd-*.json): creates the new profile on disk, optionally updates active.txt

The daemon picks up the active profile's interpretationHint (set per-profile from the extension popup → Advanced → "Interpretation hint") on every signal. Edit the hint in the popup; the next signal the daemon processes will use it. No restart needed.

After 30s of cotext signal-ing in another terminal, you'll see the new entries materialize in the profile markdown. Run cotext pull to verify.

Cloud-only mode (no folder, no extension)

If no folder is configured but COTEXT_API_TOKEN is set, the daemon runs against cotext.io directly:

# Headless server / remote machine — no browser, no folder
export COTEXT_API_TOKEN=ctx_...

# Pick a provider — same three options as folder mode:
#   - Ollama running locally, OR
#   - Anthropic via COTEXT_ANTHROPIC_KEY, OR
#   - OpenAI via COTEXT_OPENAI_KEY
export COTEXT_ANTHROPIC_KEY=sk-ant-...

# Bootstrap an initial profile if you don't have one yet
cotext profile create "Daily" --starter daily-driver --activate

# Start the daemon
cotext daemon

What changes vs folder mode:

  • The daemon polls https://cotext.io/api/signals/pending instead of <folder>/inbox/ (every 30s by default — cloud cadence is lower than folder).
  • Each interpreted signal appends a rule to the profile's prompt and pushes the updated profile back via /api/push. No file system involved.
  • No signals.jsonl mirror — the server's RemoteSignal table is the canonical signal log.

This makes the extension genuinely optional — a CLI-first user on a remote machine can run the whole loop (cotext signal → daemon interprets → profile updates) without ever opening a browser.

Full synthesis (librarian + prose passes)

After every Nth appended entry (default 3, controlled by --synth-every), the daemon runs the same two-pass synthesis the extension does: a librarian pass to dedupe and group rules into a clean library, then a prose pass to write the "Core Style" summary paragraph. The output matches what you'd get from opening the popup and pulling — same prompt header, same metric anchors, same section layout — so the daemon's polished render is indistinguishable from the extension's.

Spend control:

cotext daemon --synth-every 0   # disable cadence — run cotext synth manually
cotext daemon --synth-every 10  # synthesize once every 10 entries instead
cotext synth                    # one-off synthesis pass, any time

Cloud-provider users (Anthropic / OpenAI) usually want a higher --synth-every since each pass costs two extra LLM round-trips per signal. Ollama users can leave it at the default.

Daemon limitations (v1)

  • No screenshots / images. Vision-aware interpretation is extension-only today — the daemon ignores the screenshotDataUrl field on signals.

Don't run the daemon AND the extension at once

Both processes watch the same folder. The race on inbox-file deletes is harmless (whoever wins, wins), but two writers to the profile JSON can clobber each other. Pick one:

  • Browser extension only — default workflow. Use the extension's popup, occasionally use the CLI to read or to queue signals.
  • Daemon only — headless / server / CLI-first workflow.

The daemon refuses to start if another daemon is already running on the same folder (PID lockfile at <folder>/.cotext-daemon.lock). There's no equivalent guard against the extension; respect the rule manually until coordination ships.

Running as a background service

The daemon is a foreground process by default. To run it under a service manager:

macOS (launchd) — drop a plist at ~/Library/LaunchAgents/io.cotext.daemon.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>io.cotext.daemon</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/cotext</string>
    <string>daemon</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>StandardOutPath</key><string>/tmp/cotext-daemon.log</string>
  <key>StandardErrorPath</key><string>/tmp/cotext-daemon.err</string>
</dict>
</plist>

Then launchctl load ~/Library/LaunchAgents/io.cotext.daemon.plist.

Linux (systemd) — drop a unit at ~/.config/systemd/user/cotext-daemon.service:

[Unit]
Description=Cotext daemon
After=network.target

[Service]
ExecStart=/usr/local/bin/cotext daemon
Restart=on-failure

[Install]
WantedBy=default.target

Then systemctl --user enable --now cotext-daemon.

Source discovery

Priority chain (highest first):

  1. --folder <path> flag → folder source
  2. COTEXT_FOLDER environment variable → folder source
  3. ~/.config/cotext/config.json (or $XDG_CONFIG_HOME/cotext/config.json) → folder source
  4. COTEXT_API_TOKEN environment variable → cloud source (https://cotext.io, override with COTEXT_API_URL)

Folder always wins when both are set — connecting a folder switches you off the cloud automatically.

MCP server

cotext-mcp speaks MCP over stdio and exposes the active profile to any compatible client.

Resources

URIDescription
cotext://preferences/activeActive profile's synthesized prompt (markdown)
cotext://profilesJSON summary of every profile
cotext://profiles/{slug}One profile's prompt by slug

Tools

NameArgsReturns
get_preferences{ profile?: string }Prompt text for the given (or active) profile
list_profilesJSON summary list
record_signal{ type?, tag?, note?, aiResponse?, userPrompt?, profile? }Drops a feedback signal into the inbox (folder source) or POST /api/signals (cloud source).

The expectation is that the assistant calls record_signal whenever the user expresses a preference ("be more concise", "this format works") so the loop closes back into the profile pipeline.

Wiring (one command)

cotext mcp-install

Registers cotext-mcp with Claude Code (~/.claude.json) and Codex (~/.codex/config.toml) in one go. Idempotent — re-running does nothing if the entry already matches. Skips Codex silently if ~/.codex doesn't exist.

cotext mcp-install --claude         # only Claude Code
cotext mcp-install --codex          # only Codex
cotext mcp-install --print-only     # show the snippets, don't write

mcp-install writes the absolute path of cotext-mcp.js and the Node binary running the install, so the MCP client doesn't need anything on its PATH to find the server. Restart your AI client after running this.

Teaching the agent to use it

The MCP server is wired, but the agent still needs to know when to call get_preferences (load your style) and record_signal (capture inline preferences). Pipe the canonical snippet into your CLAUDE.md (or AGENTS.md for Codex):

cotext mcp-prompt >> ~/.claude/CLAUDE.md
# or for a single project:
cotext mcp-prompt >> ./CLAUDE.md

The snippet is short, self-contained, and tells the agent:

  • Call get_preferences once at session start, apply the returned style
  • Call record_signal whenever the user expresses a preference ("be more concise", "call me John", "this format is perfect")
  • Don't ask for confirmation — the user already opted in by installing Cotext

Run cotext mcp-prompt on its own to read the full text before pasting.

Manual wiring (if you prefer)

mcp-install is doing two things you can do by hand:

Claude Code — merge into ~/.claude.json top-level mcpServers:

{
  "mcpServers": {
    "cotext": {
      "command": "cotext-mcp",
      "args": []
    }
  }
}

Codex — append to ~/.codex/config.toml:

[mcp_servers.cotext]
command = "cotext-mcp"
args = []

Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json on macOS) uses the same JSON shape as Claude Code.

For cloud-only setups (no local folder), pass the API token via env:

{
  "mcpServers": {
    "cotext": {
      "command": "cotext-mcp",
      "env": { "COTEXT_API_TOKEN": "ctx_..." }
    }
  }
}

The server re-resolves the source on every request, so prompt edits in the extension (or remote profile changes) show up live without a client restart.

How the folder is populated

The browser extension writes one <slug>.json + <slug>.md pair per profile, plus active.txt pointing at the current one. This package only writes to <folder>/inbox/ (signals); profile artifacts are owned by the extension.

Cloud surface

CloudSource calls the following endpoints on cotext.io (or COTEXT_API_URL), all authenticated with Authorization: Bearer ${COTEXT_API_TOKEN}:

MethodPathBody / ResultStatus
GET/api/preferences/active{ profile: ProfileBundle }shipped
GET/api/profiles{ profiles: ProfileSummary[] }shipped
GET/api/profiles/{slug}{ profile: ProfileBundle }shipped
POST/api/signals{ ...RecordSignalInput }{ id }shipped
GET/api/signals/pending{ signals: [...], hasMore }shipped
POST/api/signals/{id}/drained{ ok: true }shipped

All endpoints are shipped. The signals surface is a cloud inbox, not a server-side interpreter: POST /api/signals accepts + stores; a local drainer (browser extension service worker, or cotext daemon with cloud credentials) polls /pending, runs LLM interpretation locally, then calls /{id}/drained to mark each one consumed. The LLM stays on the user's machine — same privacy posture as folder mode.

Cloud-side signal recording works cotext signal against a cloud-only configuration today; you'll need a local drainer running somewhere (browser extension OR daemon) for those signals to actually fold into the profile. Without a drainer the signals just queue at /pending.

The "active" profile is set when the extension pushes with setActive: true (or, if never set, falls back to the most recently updated profile — same semantics as a missing active.txt).

Development

npm install
npm run build:dev    # esbuild bundle, sourcemaps on
npm run watch        # rebuild on change

See also