contextspin 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +80 -248
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,331 +1,163 @@
1
1
  # ContextSpin
2
2
 
3
- Live context in your Claude Code **status bar** — weather, the top Hacker News story, PRs awaiting your review, CI failures, incidents, meetings — pulled from tools you already run. Install in one line; the bar is never empty.
3
+ Live context in your Claude Code **status bar** — weather, the top Hacker News story, PRs awaiting your review, CI failures, incidents, meetings — pulled from tools you already run. One-line install, and the bar is never empty.
4
4
 
5
5
  ```bash
6
6
  curl -fsSL https://raw.githubusercontent.com/mannutech/contextspin/main/install.sh | bash
7
7
  ```
8
8
 
9
- ## Key principle: ContextSpin does NOT fetch data
9
+ Requires Node.js 18. MIT licensed. The only runtime dependency is [`commander`](https://www.npmjs.com/package/commander).
10
10
 
11
- ContextSpin is a **compositor and renderer, not a data layer.** It has no API clients, no auth flows, and no integrations of its own. Instead it **aggregates from sources you already have**:
11
+ ## It does NOT fetch data
12
12
 
13
- - **existing MCP servers** registered in your `~/.claude.json` / `.mcp.json`
14
- - **CLI tools** already installed and authenticated on your machine (`gh`, `kubectl`, `aws`, your own scripts…)
15
- - **HTTP endpoints** you can already reach (internal dashboards, status APIs)
13
+ ContextSpin is a **renderer, not a data layer** no API clients, no auth flows, no integrations of its own. It aggregates from sources you already have:
16
14
 
17
- ContextSpin formats whatever your sources return into short one-line snippets and shows the most relevant one in the Claude Code status bar. If a piece of data isn't reachable by a tool you already have, ContextSpin cannot show it — by design. The only runtime dependency is [`commander`](https://www.npmjs.com/package/commander).
15
+ - **MCP servers** registered in `~/.claude.json` / `.mcp.json` (stdio only)
16
+ - **CLI tools** already installed and authed (`gh`, `kubectl`, `glab`, your scripts…)
17
+ - **HTTP endpoints** you can already reach
18
18
 
19
- ## Architecture (daemonless)
19
+ It formats whatever they return into one-line snippets and shows the most relevant one. If a tool you have can't reach the data, ContextSpin can't show it — by design.
20
20
 
21
- There is **no background daemon by default**. The statusline render is the engine: it serves the cached snippet instantly, and only when a source is due does it kick off a detached one-shot refresh (lock-guarded so frequent renders never overlap). Nothing runs when you're not in Claude Code — idle cost is zero.
21
+ ## How it works (daemonless)
22
+
23
+ There is **no background process by default**. The statusline render is the engine — it serves the cached snippet instantly, and only when a source is past its cooldown does it spawn a detached one-shot refresh (lock-guarded so frequent renders never overlap). Nothing runs when you're not in Claude Code, so idle cost is zero.
22
24
 
23
25
  ```
24
- Claude Code draws the status bar (every refreshInterval)
25
-
26
-
27
- ┌─────────────────────────────────────────────────────────┐
28
- │ RENDER ~/.contextspin/statusline.mjs │
29
- 1. read ~/.contextspin-cache.json │
30
- 2. print one snippet NOW (stale is fine) ───────────────┼──► status bar
31
- │ 3. if a source is past its cooldown AND no refresh is │
32
- │ in flight → spawn a detached one-shot refresh: │
33
- └───────────────────────────┬─────────────────────────────┘
34
- │ (background, non-blocking)
35
-
36
- ┌─────────────────────────────────────────────────────────┐
37
- │ REFRESH (one-shot) src/refresh-entry.js │
38
- │ • runs each DUE source (cli / http / mcp), formats │
39
- │ • merges / dedups / prioritizes, records lastRun │
40
- │ • writes ~/.contextspin-cache.json (atomic) │
41
- └─────────────────────────────────────────────────────────┘
26
+ Claude Code draws the status bar
27
+
28
+
29
+ RENDER (~/.contextspin/statusline.mjs)
30
+ 1. read the cache, print one snippet NOW (stale is fine) ──► status bar
31
+ 2. if a source is due and no refresh is in flight:
32
+
33
+ (detached, non-blocking)
34
+ REFRESH (one-shot, src/refresh-entry.js)
35
+ • run each DUE source, format, merge/dedup/prioritize, record lastRun
36
+ write ~/.contextspin-cache.json (atomic)
42
37
  ```
43
38
 
44
- This is *stale-while-revalidate*: the bar is always fast (it never waits on the network), and freshness catches up in the background. A legacy always-on **daemon** is still available behind `injection.daemonless: false` — useful only if you poll stdio **MCP** sources, where a persistent connection beats per-render handshakes.
45
-
46
- The daemon and the injector are decoupled by the cache file: the daemon writes snippets, the injector reads them. Each runs on its own clock.
39
+ This is *stale-while-revalidate*: the bar is always fast, freshness catches up in the background. A legacy always-on daemon is still available behind `injection.daemonless: false` — only worth it for stdio MCP sources, where a persistent connection beats per-render handshakes.
47
40
 
48
41
  ## Install
49
42
 
50
- Requires Node.js >= 18 (ContextSpin uses the built-in global `fetch`).
51
-
52
- **One line — that's it:**
53
-
54
43
  ```bash
55
44
  curl -fsSL https://raw.githubusercontent.com/mannutech/contextspin/main/install.sh | bash
56
45
  ```
57
46
 
58
- This wires a SessionStart hook into `~/.claude/settings.json` (so ContextSpin self-heals every session), seeds a no-credentials starter pack (weather, a dad joke, the top Hacker News story), and wires your status bar non-destructively. Restart Claude Code and you'll see live snippets, never an empty bar.
47
+ This wires a SessionStart hook into `~/.claude/settings.json` (so it self-heals each session), seeds a no-credentials starter pack (weather, a dad joke, the top HN story), and wires your status bar **non-destructively** (any existing status line is preserved and composed above ours). Restart Claude Code to see it.
59
48
 
60
- Prefer to do it yourself? `npx contextspin install` does the same thing. To remove everything: `npx contextspin uninstall`.
49
+ `npx contextspin install` does the same. `npx contextspin uninstall` removes everything. `npx contextspin status` shows the current snippets.
61
50
 
62
- <details>
63
- <summary>Manual / advanced setup</summary>
51
+ ## Sources
64
52
 
65
- ```bash
66
- npx contextspin setup # create a config (add --yes to skip prompts)
67
- npx contextspin inject # wire snippets into the status bar
68
- # that's it — the render refreshes itself; no daemon to start
69
- ```
70
-
71
- `npx contextspin install` is the recommended path (it also wires the self-healing SessionStart hook). Running `npx contextspin` with **no subcommand** sets up if unconfigured, otherwise wires + refreshes.
53
+ Every source returns a list of records. Each record is optionally `filter`ed, then rendered with `format` using `{{ field }}` templating — dotted/bracketed paths work (`{{ results[0].value }}`), `{{ env.NAME }}` reads an environment variable, unknown fields render empty.
72
54
 
73
- </details>
74
-
75
- Check what's happening at any time:
76
-
77
- ```bash
78
- npx contextspin status
79
- ```
80
-
81
- ## Source types
82
-
83
- Every source produces a list of records. Each record is run through an optional `filter`, then rendered with `format` using **double-curly-brace** field templating (`{{ field }}`). Inner whitespace is allowed. Dotted and bracketed paths work (`{{ results[0].value }}`), and `{{ env.NAME }}` resolves to an environment variable. Unknown fields render as the empty string.
84
-
85
- ### `mcp` — call a tool on an existing MCP server
86
-
87
- Calls a tool on a **stdio** MCP server discovered from your Claude config. ContextSpin speaks JSON-RPC 2.0 over the server's stdin/stdout itself — no SDK, no extra dependency.
55
+ **`mcp`** — call a tool on a stdio MCP server discovered from your Claude config (JSON-RPC over stdin/stdout, no SDK):
88
56
 
89
57
  ```json
90
- {
91
- "type": "mcp",
92
- "tool": "slack_search_public",
93
- "args": { "query": "mentions:me is:unread" },
94
- "format": "Slack: {{ text }}",
95
- "label": "Slack",
96
- "cooldown": 300,
97
- "maxSnippets": 2
98
- }
58
+ { "type": "mcp", "tool": "slack_search_public", "args": { "query": "mentions:me is:unread" },
59
+ "format": "Slack: {{ text }}", "label": "Slack", "cooldown": 300, "maxSnippets": 2 }
99
60
  ```
100
61
 
101
- - `tool` is required. You may pass a bare tool name (`slack_search_public`) or the fully-qualified `mcp__<server>__<tool>` form; the `mcp__<server>__` prefix is stripped to find the raw tool and to infer which server to use.
102
- - `server` is optional. If omitted and the tool name doesn't encode it, ContextSpin connects to each stdio server, lists its tools, and uses the first one that exposes the tool.
103
- - `args` is passed straight through as the tool-call arguments.
62
+ `tool` may be bare or `mcp__<server>__<tool>`; `server` is optional (otherwise the first stdio server exposing the tool is used).
104
63
 
105
- ### `cli` — run a shell command
64
+ **`cli`** — run a shell command (output parsed as a JSON array/object/primitive, or split into lines):
106
65
 
107
66
  ```json
108
- {
109
- "type": "cli",
110
- "command": "gh pr list --review-requested @me --json title,number --limit 3",
111
- "format": "PR #{{ number }} needs your review: {{ title }}",
112
- "label": "GitHub",
113
- "cooldown": 120,
114
- "maxSnippets": 3
115
- }
67
+ { "type": "cli", "command": "gh pr list --review-requested @me --json title,number --limit 3",
68
+ "format": "PR #{{ number }} needs review: {{ title }}", "label": "GitHub", "cooldown": 120, "maxSnippets": 3 }
116
69
  ```
117
70
 
118
- The command runs through your shell. Output parsing is forgiving:
119
-
120
- - a **JSON array** → each element becomes a record (objects kept as-is; primitives wrapped as `{ value, text }`)
121
- - a **JSON object** → a single record
122
- - a **JSON primitive** → `{ value, text }`
123
- - **anything else** → split into non-empty lines, each becoming `{ text, line, value }`
124
-
125
- A non-zero exit throws; a configurable timeout (default 15s) protects against hangs.
126
-
127
- ### `http` — fetch a JSON (or text) endpoint
71
+ **`http`** fetch a JSON or text endpoint:
128
72
 
129
73
  ```json
130
- {
131
- "type": "http",
132
- "url": "https://grafana.example.com/api/datasources/proxy/1/query?q=incidents",
74
+ { "type": "http", "url": "https://grafana.example.com/api/.../query?q=incidents",
133
75
  "headers": { "Authorization": "Bearer {{ env.GRAFANA_TOKEN }}" },
134
- "jq": ".results[0].value",
135
- "format": "Grafana: {{ value }}",
136
- "label": "Grafana",
137
- "cooldown": 30,
138
- "maxSnippets": 1
139
- }
76
+ "jq": ".results[0].value", "format": "Grafana: {{ value }}", "label": "Grafana", "cooldown": 30 }
140
77
  ```
141
78
 
142
- - `url` and header values are interpolated, so you can inject secrets with `{{ env.X }}` instead of hard-coding them.
143
- - `method` defaults to `GET`; a `body` object is JSON-stringified with the right content-type.
144
- - `jq` accepts a **minimal jq subset**: identity `.`, dotted keys (`.a.b`), bracket indexing (`.a[0]`), iteration (`.[]`, `.a[]`), and left-to-right pipes (`a | b`). Unsupported expressions pass the data through unchanged rather than erroring.
79
+ `url` and headers are interpolated (use `{{ env.X }}` for secrets, never hard-code them). `jq` supports a minimal subset: identity, dotted keys, bracket indexing, iteration (`.[]`), and pipes.
145
80
 
146
81
  ## Configuration
147
82
 
148
- ContextSpin reads one JSON file: `~/.contextspin.json` (override with the `CONTEXTSPIN_CONFIG` environment variable). The cache lives at `~/.contextspin-cache.json` (override with `CONTEXTSPIN_CACHE`).
83
+ One JSON file, `~/.contextspin.json` (override with `CONTEXTSPIN_CONFIG`); cache at `~/.contextspin-cache.json` (override with `CONTEXTSPIN_CACHE`).
149
84
 
150
85
  ```json
151
86
  {
152
87
  "sources": [
153
88
  { "type": "cli", "command": "gh pr list --json title --limit 3", "format": "PR: {{ title }}" }
154
89
  ],
155
- "injection": {
156
- "mode": "statusline",
157
- "refresh": 30,
158
- "maxVisible": 5
159
- },
160
- "snippets": {
161
- "deduplication": true,
162
- "cooldownAfterShown": 3,
163
- "priorityOrder": ["incident", "ci", "slack", "calendar", "github", "jira"]
164
- }
90
+ "injection": { "mode": "statusline", "refresh": 30, "maxVisible": 5 },
91
+ "snippets": { "deduplication": true, "cooldownAfterShown": 3, "priorityOrder": ["incident", "ci", "github"] }
165
92
  }
166
93
  ```
167
94
 
168
- ### Field reference
169
-
170
- | Field | Type | Format / values | Default | Meaning |
171
- |-------|------|-----------------|---------|---------|
172
- | `sources` | array | | `[]` | Sources to poll. May be empty (the bar then shows the built-in defaults). |
173
- | `sources[].type` | string | `mcp` \| `cli` \| `http` | | Source kind. Required. |
174
- | `sources[].tool` | string | tool name or `mcp__server__tool` | | Required for `mcp`. |
175
- | `sources[].command` | string | shell command | | Required for `cli`. |
176
- | `sources[].url` | string | URL (templated) | | Required for `http`. |
177
- | `sources[].format` | string | `{{ field }}` template | — | One-line render template. Required. |
178
- | `sources[].filter` | string | `LEFT OP RIGHT` | | Optional. Keep a record only if it passes. See below. |
179
- | `sources[].label` | string | free text | derived | Shown as the snippet source. Derived if omitted: mcp→tool name, cli→first command token, http→hostname. |
180
- | `sources[].cooldown` | number | seconds | `300` | Minimum seconds between polls of this source. |
181
- | `sources[].maxSnippets` | number | count | `2` | Max snippets kept from one poll of this source. |
182
- | `injection.mode` | string | `statusline` \| `patcher` \| `both` | `statusline` | How snippets reach the UI. |
183
- | `injection.refresh` | number | seconds | `30` | Daemon poll interval and status-line refresh interval (seconds). |
184
- | `injection.maxVisible` | number | count | `5` | Global cap on snippets held in the cache. |
185
- | `injection.style` | boolean | — | `true` | Render the line in a styled box (cyan bars + italic). Set `false` for plain text. |
186
- | `injection.daemonless` | boolean | | `true` | Render self-refreshes (stale-while-revalidate), no background process. Set `false` for the legacy always-on daemon. |
187
- | `snippets.deduplication` | boolean | — | `true` | Drop snippets with duplicate text when merging. |
188
- | `snippets.cooldownAfterShown` | number | count | `3` | A snippet stops being eligible once shown this many times. |
189
- | `snippets.priorityOrder` | string[] | source labels | `[]` | Earlier labels sort first (case-insensitive); unlisted sort last. |
190
-
191
- ### Filters
192
-
193
- `filter` is a **single, safe comparison** — no `eval`, no `Function`. The whole expression is interpolated against the record first, then parsed as `LEFT OP RIGHT` where `OP` is one of `==`, `!=`, `>=`, `<=`, `>`, `<`, or the word `includes`. Both numeric sides compare numerically; otherwise they compare as strings (`==` / `!=` are loose). `includes` is substring containment. With no operator, the result is truthy unless it's empty, `false`, or `0`.
95
+ | Field | Default | Meaning |
96
+ |-------|---------|---------|
97
+ | `sources[].type` | | `mcp` \| `cli` \| `http` (required) |
98
+ | `sources[].tool` / `command` / `url` | — | Required for `mcp` / `cli` / `http` respectively |
99
+ | `sources[].format` | | One-line `{{ field }}` template (required) |
100
+ | `sources[].filter` | | Keep a record only if it passes (see below) |
101
+ | `sources[].label` | derived | Snippet source label (mcp→tool, cli→first token, http→host) |
102
+ | `sources[].cooldown` | `300` | Min seconds between polls of this source |
103
+ | `sources[].maxSnippets` | `2` | Max snippets kept per poll |
104
+ | `injection.refresh` | `30` | Status-bar refresh interval, seconds |
105
+ | `injection.maxVisible` | `5` | Cap on snippets held in the cache |
106
+ | `injection.style` | `true` | Styled box (cyan bars + italic); `false` for plain text |
107
+ | `injection.daemonless` | `true` | Self-refreshing render; `false` for the legacy daemon |
108
+ | `injection.mode` | `statusline` | `statusline` \| `patcher` \| `both` |
109
+ | `snippets.deduplication` | `true` | Drop duplicate-text snippets when merging |
110
+ | `snippets.cooldownAfterShown` | `3` | A snippet stops showing after this many displays |
111
+ | `snippets.priorityOrder` | `[]` | Source labels sorted first (case-insensitive); rest last |
112
+
113
+ **Filters** are a single safe comparison (no `eval`): the expression is interpolated, then parsed as `LEFT OP RIGHT` where `OP` is `==`, `!=`, `>=`, `<=`, `>`, `<`, or `includes`. No `&&`/`||`.
194
114
 
195
115
  ```json
196
116
  { "filter": "{{ status }} == failure" }
197
117
  ```
198
118
 
199
- Only one comparison is supported — there is no `&&` / `||`.
200
-
201
- ## Injection modes
202
-
203
- ### `statusline` (recommended, official)
204
-
205
- This is the supported path. It uses Claude Code's official [status line](https://code.claude.com/docs/en/statusline) feature, so it survives Claude Code updates.
206
-
207
- `contextspin inject` (mode `statusline`) will:
208
-
209
- 1. Write `~/.contextspin/statusline-render.js` — a self-contained script that drains stdin (so Claude Code's piped JSON can't cause `EPIPE`), reads the cache, picks the eligible snippet with the lowest `shownCount` (then most recent), increments its count, writes the cache back, and prints that one line. Any error exits cleanly with no output, so it can never break your status bar.
210
- 2. Write `~/.contextspin/statusline.sh` — a `0755` bash wrapper that `exec`s the render script.
211
- 3. Point `statusLine` at that wrapper (refresh in **seconds**), **non-destructively**: any status line you already had is preserved and *composed* — the render script runs your prior command and prints its output **above** the ContextSpin line. Scope-aware: in a project (when `CLAUDE_PROJECT_DIR` is set) it writes the gitignored `<project>/.claude/settings.local.json`, which outranks a repo's tracked `settings.json`, so a project's own status line can't shadow ContextSpin.
212
-
213
- Reverse it with `contextspin uninject` (this scope) or `contextspin uninstall` (every scope it ever wired, plus the hook and daemon).
214
-
215
- ### `patcher` (EXPERIMENTAL — binary patching)
216
-
217
- > ⚠️ **Experimental and fragile.** Inspired by [claude-depester](https://github.com/ominiverdi/claude-depester). It rewrites the hard-coded spinner words (`Flibbertigibbeting`, `Discombobulating`, …) inside the Claude Code binary/bundle with your live snippets.
218
-
219
- Key facts:
220
-
221
- - It is **length-preserving**: the replacement is padded with spaces so the file size never changes. If your snippets don't fit, words are trimmed until they do.
222
- - It works on both **text** (`cli.js`) and **compiled binary** installs, located by scanning the usual install paths and keeping only files that actually contain the marker word.
223
- - A **restart of Claude Code is required** for the patch to take effect.
224
- - **Claude Code updates overwrite the patch.** The installer also writes a wrapper at `~/.contextspin/cl.sh` that re-applies the patch and then `exec`s `claude`. After every Claude Code update you must re-patch — using that wrapper is the easiest way.
225
- - On macOS it makes a best-effort `codesign` re-sign after patching.
226
-
227
- Restore the originals with `contextspin uninject --mode patcher` (or `inject --mode both` / `uninject --mode both` to do both at once). A backup with the suffix `.contextspin.backup` is created before any install is touched.
228
-
229
- ## Refresh and cache
230
-
231
- By default there is **no background process**. Each time the status bar draws, the render:
232
-
233
- 1. Prints the current cached snippet immediately (stale is fine).
234
- 2. If any source is past its `cooldown` and no refresh is already in flight (a lock at `~/.contextspin/refresh.lock`, TTL 60s), spawns a **detached one-shot refresh**. That refresh runs only the due sources, merges into the existing set (preserves `shownCount` for matching text, optionally dedups, sorts by `priorityOrder` then recency, caps to `injection.maxVisible`), records per-source `lastRun`, and atomically writes the cache.
235
-
236
- Set `injection.daemonless: false` to use the **legacy daemon** instead: `contextspin start` spawns a detached background process (PID at `~/.contextspin/daemon.pid`, logs at `~/.contextspin/daemon.log`) that polls on `injection.refresh` and writes the same cache. Use this only if you poll stdio MCP sources.
237
-
238
- ### Cache file format (`~/.contextspin-cache.json`)
119
+ ## Cache
239
120
 
240
121
  ```json
241
122
  {
242
123
  "updatedAt": "2026-06-17T09:00:00.000Z",
243
124
  "snippets": [
244
- {
245
- "text": "CI failing: build on main",
246
- "source": "CI",
247
- "sourceId": 2,
248
- "fetchedAt": "2026-06-17T09:00:00.000Z",
249
- "shownCount": 0
250
- }
125
+ { "text": "CI failing: build on main", "source": "CI", "sourceId": 2,
126
+ "fetchedAt": "2026-06-17T09:00:00.000Z", "shownCount": 0 }
251
127
  ],
252
128
  "meta": { "lastRun": { "2": 1781860451773 } }
253
129
  }
254
130
  ```
255
131
 
256
- `shownCount` is incremented by the render each time a snippet is displayed; once it reaches `cooldownAfterShown` the snippet is no longer shown. `meta.lastRun` maps `sourceId → last poll (epoch ms)` so the daemonless refresh honors per-source cooldowns across runs.
132
+ `shownCount` rises each time a snippet is shown; past `cooldownAfterShown` it's retired. `meta.lastRun` maps `sourceId → last poll (ms)` so the refresh honors per-source cooldowns across runs. When the cache is empty or every snippet is retired, the render rotates through built-in defaults (jokes + tips) — so the bar is never blank.
257
133
 
258
- ## CLI commands
134
+ ## CLI
259
135
 
260
136
  | Command | What it does |
261
137
  |---------|--------------|
262
- | `contextspin install` | **One-shot install:** wire a self-healing SessionStart hook, create the config, and wire the statusline. (This is what the curl script runs.) |
263
- | `contextspin uninstall` | **Full teardown:** remove the hook, restore your prior statusline in **every** scope it wired, and stop any daemon. |
264
- | `contextspin setup [--yes]` | Create `~/.contextspin.json` (interactive, or a detected config with `--yes` / non-TTY). |
265
- | `contextspin status` | Show the engine, and the current cached snippets (source, age, shown count). |
266
- | `contextspin ensure` | Idempotent: create config + wire statusline (run by the SessionStart hook each session). |
267
- | `contextspin inject [--mode <m>]` | Install just the injector. `<m>` overrides `injection.mode` (`statusline` / `patcher` / `both`). |
268
- | `contextspin uninject [--mode <m>]` | Reverse just the injector. |
269
- | `contextspin start` / `stop` / `restart` | Manage the **legacy daemon** (only when `injection.daemonless: false`). |
270
- | `contextspin` *(no subcommand)* | `setup` if unconfigured, otherwise wire + refresh. |
138
+ | `install` | Wire the self-healing SessionStart hook, create config, wire the statusline (what the curl script runs). |
139
+ | `uninstall` | Remove the hook, restore your prior statusline in **every** scope, stop any daemon. |
140
+ | `status` | Show the engine and cached snippets. |
141
+ | `refresh` | Force a one-shot refresh of all due sources now. |
142
+ | `setup [--yes]` | Create `~/.contextspin.json` (interactive, or detected with `--yes`). |
143
+ | `ensure` | Idempotent create-config + wire-statusline (run by the hook each session). |
144
+ | `inject` / `uninject [--mode <m>]` | Install / reverse just the injector. |
145
+ | `start` / `stop` / `restart` | Manage the legacy daemon (only when `injection.daemonless: false`). |
271
146
 
272
- ## High-impact snippets
147
+ ## Statusline injection
273
148
 
274
- Three tiers, by how time-sensitive they are.
275
-
276
- ### Tier 1 — time-sensitive (act in minutes)
277
-
278
- ```json
279
- { "type": "cli", "command": "gh run list --json status,name,headBranch --limit 5",
280
- "filter": "{{ status }} == failure",
281
- "format": "CI failing: {{ name }} on {{ headBranch }}", "label": "CI", "cooldown": 60, "maxSnippets": 2 }
282
- ```
283
-
284
- ```json
285
- { "type": "http", "url": "https://grafana.example.com/api/datasources/proxy/1/query?q=incidents",
286
- "headers": { "Authorization": "Bearer {{ env.GRAFANA_TOKEN }}" },
287
- "jq": ".results[0].value", "format": "Grafana: {{ value }}", "label": "Grafana", "cooldown": 30, "maxSnippets": 1 }
288
- ```
149
+ Uses Claude Code's official [status line](https://code.claude.com/docs/en/statusline) feature, so it survives updates. The wrapper is **non-destructive and scope-aware**: any status line you already had is composed above the ContextSpin line, and in a project (`CLAUDE_PROJECT_DIR` set) it writes the gitignored `<project>/.claude/settings.local.json` so a repo's own status line can't shadow it. Reverse with `uninject` (this scope) or `uninstall` (everything).
289
150
 
290
- ### Tier 2ambient ops (good to know)
291
-
292
- ```json
293
- { "type": "mcp", "tool": "slack_search_public", "args": { "query": "mentions:me is:unread" },
294
- "format": "Slack: {{ text }}", "label": "Slack", "cooldown": 300, "maxSnippets": 2 }
295
- ```
296
-
297
- ```json
298
- { "type": "mcp", "tool": "notion-search", "args": { "query": "assigned:me status:open" },
299
- "format": "Notion: {{ text }}", "label": "Notion", "cooldown": 300, "maxSnippets": 2 }
300
- ```
301
-
302
- ### Tier 3 — work queue (your to-do)
303
-
304
- ```json
305
- { "type": "cli", "command": "gh pr list --review-requested @me --json title,number --limit 3",
306
- "format": "PR #{{ number }} needs your review: {{ title }}", "label": "GitHub", "cooldown": 120, "maxSnippets": 3 }
307
- ```
151
+ There's also an **experimental** `patcher` mode (`injection.mode: "patcher"`) that rewrites Claude Code's hard-coded spinner words in the binary inspired by [claude-depester](https://github.com/ominiverdi/claude-depester). It's length-preserving and best-effort, but **every Claude Code update overwrites it**, so the statusline is the supported path. Restore with `uninject --mode patcher`.
308
152
 
309
153
  ## Limitations
310
154
 
311
- - **MCP support is stdio-only.** ContextSpin discovers MCP servers from `~/.claude.json` (user and per-project scopes) and `.mcp.json`, and connects only to **stdio** servers (those with a `command`). HTTP / SSE / WebSocket MCP transports are not supported use a `cli` or `http` source instead. Plugin / managed scopes are ignored.
312
- - **OAuth-based claude.ai connectors are not reachable.** App-connected connectors (Slack, Notion, etc. linked through claude.ai) authenticate via OAuth tokens stored in the OS keychain. A standalone background daemon has no access to those tokens, so it cannot drive those connectors. Use the corresponding CLI (`gh`, `slack` CLI…) or HTTP endpoint, or a locally-configured stdio MCP server, instead.
313
- - **The status line shows one rotating snippet** at a time, honoring `cooldownAfterShown` so the same item doesn't repeat indefinitely.
314
- - **The patcher is experimental** and is **overwritten by every Claude Code update**. Treat it as best-effort; the statusline mode is the supported path.
315
-
316
- ## Zero-config defaults (never an empty bar)
317
-
318
- A fresh install needs no setup:
319
-
320
- - The config is seeded with a **no-credentials starter pack** — local weather, a dad joke, and the top Hacker News story — so real snippets appear within seconds.
321
- - When the cache is empty or every snippet is exhausted, the renderer falls back to **built-in defaults** (jokes + "ask `/contextspin`…" tips) that rotate, so the bar is never blank — even offline or before the first poll.
322
- - A Claude Code **plugin** is also available (the [`mannutech` marketplace](https://github.com/mannutech/claude-plugins)) for those who prefer installing that way — it wraps this same package.
155
+ - **MCP is stdio-only** discovered from `~/.claude.json` / `.mcp.json`; HTTP/SSE MCP transports aren't supported (use a `cli`/`http` source instead).
156
+ - **OAuth claude.ai connectors aren't reachable** their tokens live in the OS keychain, out of reach of a standalone process. Use the matching CLI (`gh`, …), an HTTP endpoint, or a local stdio MCP server.
323
157
 
324
- ## References
158
+ ## Also available as a plugin
325
159
 
326
- - claude-depester (patcher inspiration): https://github.com/ominiverdi/claude-depester
327
- - Claude Code status line docs: https://code.claude.com/docs/en/statusline
328
- - Claude Code spinner issues: [#10420](https://github.com/anthropics/claude-code/issues/10420), [#13725](https://github.com/anthropics/claude-code/issues/13725), [#22668](https://github.com/anthropics/claude-code/issues/22668), [#27766](https://github.com/anthropics/claude-code/issues/27766), [#27976](https://github.com/anthropics/claude-code/issues/27976)
160
+ A Claude Code plugin wraps this package, via the [`mannutech` marketplace](https://github.com/mannutech/claude-plugins) — for those who prefer installing that way. The curl line above needs neither.
329
161
 
330
162
  ## License
331
163
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contextspin",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Replace Claude Code spinner/statusline text with live org context (meetings, Slack, CI, incidents, PRs) aggregated from your existing MCP servers, CLIs, and HTTP endpoints.",
5
5
  "type": "module",
6
6
  "bin": {