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.
- package/README.md +80 -248
- 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.
|
|
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
|
-
|
|
9
|
+
Requires Node.js ≥ 18. MIT licensed. The only runtime dependency is [`commander`](https://www.npmjs.com/package/commander).
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## It does NOT fetch data
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
49
|
+
`npx contextspin install` does the same. `npx contextspin uninstall` removes everything. `npx contextspin status` shows the current snippets.
|
|
61
50
|
|
|
62
|
-
|
|
63
|
-
<summary>Manual / advanced setup</summary>
|
|
51
|
+
## Sources
|
|
64
52
|
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
|
171
|
-
|
|
172
|
-
| `sources` |
|
|
173
|
-
| `sources[].
|
|
174
|
-
| `sources[].
|
|
175
|
-
| `sources[].
|
|
176
|
-
| `sources[].
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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`
|
|
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
|
|
134
|
+
## CLI
|
|
259
135
|
|
|
260
136
|
| Command | What it does |
|
|
261
137
|
|---------|--------------|
|
|
262
|
-
| `
|
|
263
|
-
| `
|
|
264
|
-
| `
|
|
265
|
-
| `
|
|
266
|
-
| `
|
|
267
|
-
| `
|
|
268
|
-
| `
|
|
269
|
-
| `
|
|
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
|
-
##
|
|
147
|
+
## Statusline injection
|
|
273
148
|
|
|
274
|
-
|
|
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
|
-
|
|
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
|
|
312
|
-
- **OAuth
|
|
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
|
-
##
|
|
158
|
+
## Also available as a plugin
|
|
325
159
|
|
|
326
|
-
|
|
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.
|
|
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": {
|