contextspin 0.1.0
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/.contextspin.example.json +72 -0
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/package.json +40 -0
- package/src/cli.js +492 -0
- package/src/config.js +232 -0
- package/src/daemon-entry.js +8 -0
- package/src/daemon.js +294 -0
- package/src/formatter.js +166 -0
- package/src/inject/patcher.js +757 -0
- package/src/inject/statusline.js +310 -0
- package/src/runner.js +69 -0
- package/src/sources/cli.js +148 -0
- package/src/sources/http.js +294 -0
- package/src/sources/mcp.js +586 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sources": [
|
|
3
|
+
{
|
|
4
|
+
"type": "mcp",
|
|
5
|
+
"tool": "slack_search_public",
|
|
6
|
+
"args": {
|
|
7
|
+
"query": "mentions:me is:unread"
|
|
8
|
+
},
|
|
9
|
+
"format": "Slack: {{ text }}",
|
|
10
|
+
"label": "Slack",
|
|
11
|
+
"cooldown": 300,
|
|
12
|
+
"maxSnippets": 2
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"type": "cli",
|
|
16
|
+
"command": "gh pr list --review-requested @me --json title,number --limit 3",
|
|
17
|
+
"format": "PR #{{ number }} needs your review: {{ title }}",
|
|
18
|
+
"label": "GitHub",
|
|
19
|
+
"cooldown": 120,
|
|
20
|
+
"maxSnippets": 3
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"type": "cli",
|
|
24
|
+
"command": "gh run list --json status,name,headBranch --limit 5",
|
|
25
|
+
"filter": "{{ status }} == failure",
|
|
26
|
+
"format": "CI failing: {{ name }} on {{ headBranch }}",
|
|
27
|
+
"label": "CI",
|
|
28
|
+
"cooldown": 60,
|
|
29
|
+
"maxSnippets": 2
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "mcp",
|
|
33
|
+
"tool": "notion-search",
|
|
34
|
+
"args": {
|
|
35
|
+
"query": "assigned:me status:open"
|
|
36
|
+
},
|
|
37
|
+
"format": "Notion: {{ text }}",
|
|
38
|
+
"label": "Notion",
|
|
39
|
+
"cooldown": 300,
|
|
40
|
+
"maxSnippets": 2
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"type": "http",
|
|
44
|
+
"url": "https://grafana.example.com/api/datasources/proxy/1/query?q=incidents",
|
|
45
|
+
"headers": {
|
|
46
|
+
"Authorization": "Bearer {{ env.GRAFANA_TOKEN }}"
|
|
47
|
+
},
|
|
48
|
+
"jq": ".results[0].value",
|
|
49
|
+
"format": "Grafana: {{ value }}",
|
|
50
|
+
"label": "Grafana",
|
|
51
|
+
"cooldown": 30,
|
|
52
|
+
"maxSnippets": 1
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"injection": {
|
|
56
|
+
"mode": "statusline",
|
|
57
|
+
"refresh": 30,
|
|
58
|
+
"maxVisible": 5
|
|
59
|
+
},
|
|
60
|
+
"snippets": {
|
|
61
|
+
"deduplication": true,
|
|
62
|
+
"cooldownAfterShown": 3,
|
|
63
|
+
"priorityOrder": [
|
|
64
|
+
"incident",
|
|
65
|
+
"ci",
|
|
66
|
+
"slack",
|
|
67
|
+
"calendar",
|
|
68
|
+
"github",
|
|
69
|
+
"jira"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ContextSpin contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# ContextSpin
|
|
2
|
+
|
|
3
|
+
Replace the Claude Code spinner / status bar text with live org context — meetings, Slack mentions, CI failures, incidents, review queues — pulled from tools you already run.
|
|
4
|
+
|
|
5
|
+
## Key principle: ContextSpin does NOT fetch data
|
|
6
|
+
|
|
7
|
+
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**:
|
|
8
|
+
|
|
9
|
+
- **existing MCP servers** registered in your `~/.claude.json` / `.mcp.json`
|
|
10
|
+
- **CLI tools** already installed and authenticated on your machine (`gh`, `kubectl`, `aws`, your own scripts…)
|
|
11
|
+
- **HTTP endpoints** you can already reach (internal dashboards, status APIs)
|
|
12
|
+
|
|
13
|
+
ContextSpin polls those sources on a schedule, formats whatever they return into short one-line snippets, and injects the most relevant one into 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).
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌─────────────────────────────────────────────────────────┐
|
|
19
|
+
│ SOURCES (things you already have) │
|
|
20
|
+
│ │
|
|
21
|
+
│ mcp ──► stdio MCP servers from ~/.claude.json │
|
|
22
|
+
│ cli ──► shell commands (gh, kubectl, scripts...) │
|
|
23
|
+
│ http ──► HTTP/JSON endpoints you can reach │
|
|
24
|
+
└───────────────────────────┬─────────────────────────────┘
|
|
25
|
+
│ poll on per-source cooldown
|
|
26
|
+
▼
|
|
27
|
+
┌─────────────────────────────────────────────────────────┐
|
|
28
|
+
│ POLLING DAEMON (detached background process) │
|
|
29
|
+
│ • runs each source, applies filter + format │
|
|
30
|
+
│ • merges / dedups / prioritizes snippets │
|
|
31
|
+
│ • writes ~/.contextspin-cache.json (atomic) │
|
|
32
|
+
└───────────────────────────┬─────────────────────────────┘
|
|
33
|
+
│ read cache
|
|
34
|
+
▼
|
|
35
|
+
┌─────────────────────────────────────────────────────────┐
|
|
36
|
+
│ INJECTOR │
|
|
37
|
+
│ statusline ──► ~/.contextspin/statusline.sh │
|
|
38
|
+
│ patches ~/.claude/settings.json │
|
|
39
|
+
│ patcher ──► rewrites spinner words in the binary │
|
|
40
|
+
│ (EXPERIMENTAL) │
|
|
41
|
+
└───────────────────────────┬─────────────────────────────┘
|
|
42
|
+
│
|
|
43
|
+
▼
|
|
44
|
+
Claude Code spinner / status bar shows one snippet
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
## Install / Quickstart
|
|
50
|
+
|
|
51
|
+
Requires Node.js >= 18 (ContextSpin uses the built-in global `fetch`).
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# 1. Create a config (interactive, or non-interactive with --yes)
|
|
55
|
+
npx contextspin setup
|
|
56
|
+
|
|
57
|
+
# 2. Start the background polling daemon
|
|
58
|
+
npx contextspin start
|
|
59
|
+
|
|
60
|
+
# 3. Wire the snippets into the Claude Code status bar
|
|
61
|
+
npx contextspin inject
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Running `npx contextspin` with **no subcommand** is a shortcut: if no config exists it runs `setup`, otherwise it runs `start` followed by `inject` using the mode from your config.
|
|
65
|
+
|
|
66
|
+
Check what's happening at any time:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx contextspin status
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Source types
|
|
73
|
+
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
### `mcp` — call a tool on an existing MCP server
|
|
77
|
+
|
|
78
|
+
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.
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"type": "mcp",
|
|
83
|
+
"tool": "slack_search_public",
|
|
84
|
+
"args": { "query": "mentions:me is:unread" },
|
|
85
|
+
"format": "Slack: {{ text }}",
|
|
86
|
+
"label": "Slack",
|
|
87
|
+
"cooldown": 300,
|
|
88
|
+
"maxSnippets": 2
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- `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.
|
|
93
|
+
- `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.
|
|
94
|
+
- `args` is passed straight through as the tool-call arguments.
|
|
95
|
+
|
|
96
|
+
### `cli` — run a shell command
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"type": "cli",
|
|
101
|
+
"command": "gh pr list --review-requested @me --json title,number --limit 3",
|
|
102
|
+
"format": "PR #{{ number }} needs your review: {{ title }}",
|
|
103
|
+
"label": "GitHub",
|
|
104
|
+
"cooldown": 120,
|
|
105
|
+
"maxSnippets": 3
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The command runs through your shell. Output parsing is forgiving:
|
|
110
|
+
|
|
111
|
+
- a **JSON array** → each element becomes a record (objects kept as-is; primitives wrapped as `{ value, text }`)
|
|
112
|
+
- a **JSON object** → a single record
|
|
113
|
+
- a **JSON primitive** → `{ value, text }`
|
|
114
|
+
- **anything else** → split into non-empty lines, each becoming `{ text, line, value }`
|
|
115
|
+
|
|
116
|
+
A non-zero exit throws; a configurable timeout (default 15s) protects against hangs.
|
|
117
|
+
|
|
118
|
+
### `http` — fetch a JSON (or text) endpoint
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"type": "http",
|
|
123
|
+
"url": "https://grafana.example.com/api/datasources/proxy/1/query?q=incidents",
|
|
124
|
+
"headers": { "Authorization": "Bearer {{ env.GRAFANA_TOKEN }}" },
|
|
125
|
+
"jq": ".results[0].value",
|
|
126
|
+
"format": "Grafana: {{ value }}",
|
|
127
|
+
"label": "Grafana",
|
|
128
|
+
"cooldown": 30,
|
|
129
|
+
"maxSnippets": 1
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- `url` and header values are interpolated, so you can inject secrets with `{{ env.X }}` instead of hard-coding them.
|
|
134
|
+
- `method` defaults to `GET`; a `body` object is JSON-stringified with the right content-type.
|
|
135
|
+
- `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.
|
|
136
|
+
|
|
137
|
+
## Configuration
|
|
138
|
+
|
|
139
|
+
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`).
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"sources": [
|
|
144
|
+
{ "type": "cli", "command": "gh pr list --json title --limit 3", "format": "PR: {{ title }}" }
|
|
145
|
+
],
|
|
146
|
+
"injection": {
|
|
147
|
+
"mode": "statusline",
|
|
148
|
+
"refresh": 30,
|
|
149
|
+
"maxVisible": 5
|
|
150
|
+
},
|
|
151
|
+
"snippets": {
|
|
152
|
+
"deduplication": true,
|
|
153
|
+
"cooldownAfterShown": 3,
|
|
154
|
+
"priorityOrder": ["incident", "ci", "slack", "calendar", "github", "jira"]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Field reference
|
|
160
|
+
|
|
161
|
+
| Field | Type | Format / values | Default | Meaning |
|
|
162
|
+
|-------|------|-----------------|---------|---------|
|
|
163
|
+
| `sources` | array | non-empty | — | List of sources to poll. Required. |
|
|
164
|
+
| `sources[].type` | string | `mcp` \| `cli` \| `http` | — | Source kind. Required. |
|
|
165
|
+
| `sources[].tool` | string | tool name or `mcp__server__tool` | — | Required for `mcp`. |
|
|
166
|
+
| `sources[].command` | string | shell command | — | Required for `cli`. |
|
|
167
|
+
| `sources[].url` | string | URL (templated) | — | Required for `http`. |
|
|
168
|
+
| `sources[].format` | string | `{{ field }}` template | — | One-line render template. Required. |
|
|
169
|
+
| `sources[].filter` | string | `LEFT OP RIGHT` | — | Optional. Keep a record only if it passes. See below. |
|
|
170
|
+
| `sources[].label` | string | free text | derived | Shown as the snippet source. Derived if omitted: mcp→tool name, cli→first command token, http→hostname. |
|
|
171
|
+
| `sources[].cooldown` | number | seconds | `300` | Minimum seconds between polls of this source. |
|
|
172
|
+
| `sources[].maxSnippets` | number | count | `2` | Max snippets kept from one poll of this source. |
|
|
173
|
+
| `injection.mode` | string | `statusline` \| `patcher` \| `both` | `statusline` | How snippets reach the UI. |
|
|
174
|
+
| `injection.refresh` | number | seconds | `30` | Daemon poll interval and status-line refresh interval (seconds). |
|
|
175
|
+
| `injection.maxVisible` | number | count | `5` | Global cap on snippets held in the cache. |
|
|
176
|
+
| `snippets.deduplication` | boolean | — | `true` | Drop snippets with duplicate text when merging. |
|
|
177
|
+
| `snippets.cooldownAfterShown` | number | count | `3` | A snippet stops being eligible once shown this many times. |
|
|
178
|
+
| `snippets.priorityOrder` | string[] | source labels | `[]` | Earlier labels sort first (case-insensitive); unlisted sort last. |
|
|
179
|
+
|
|
180
|
+
### Filters
|
|
181
|
+
|
|
182
|
+
`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`.
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{ "filter": "{{ status }} == failure" }
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Only one comparison is supported — there is no `&&` / `||`.
|
|
189
|
+
|
|
190
|
+
## Injection modes
|
|
191
|
+
|
|
192
|
+
### `statusline` (recommended, official)
|
|
193
|
+
|
|
194
|
+
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.
|
|
195
|
+
|
|
196
|
+
`contextspin inject` (mode `statusline`) will:
|
|
197
|
+
|
|
198
|
+
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.
|
|
199
|
+
2. Write `~/.contextspin/statusline.sh` — a `0755` bash wrapper that `exec`s the render script.
|
|
200
|
+
3. Patch `~/.claude/settings.json` to set `statusLine` to `{ type: "command", command: "<statusline.sh>", padding: 0, refreshInterval: <refresh> }` (refresh is in **seconds**). If you already had a different status line, it is backed up to `~/.claude/settings.json.contextspin.bak` first.
|
|
201
|
+
|
|
202
|
+
Reverse it with `contextspin uninject` (restores your previous status line if a backup exists).
|
|
203
|
+
|
|
204
|
+
### `patcher` (EXPERIMENTAL — binary patching)
|
|
205
|
+
|
|
206
|
+
> ⚠️ **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.
|
|
207
|
+
|
|
208
|
+
Key facts:
|
|
209
|
+
|
|
210
|
+
- 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.
|
|
211
|
+
- 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.
|
|
212
|
+
- A **restart of Claude Code is required** for the patch to take effect.
|
|
213
|
+
- **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.
|
|
214
|
+
- On macOS it makes a best-effort `codesign` re-sign after patching.
|
|
215
|
+
|
|
216
|
+
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.
|
|
217
|
+
|
|
218
|
+
## Daemon and cache
|
|
219
|
+
|
|
220
|
+
`contextspin start` spawns a **detached** background process (the daemon). It writes its PID to `~/.contextspin/daemon.pid` and logs to `~/.contextspin/daemon.log`. The loop:
|
|
221
|
+
|
|
222
|
+
1. For each source whose `cooldown` has elapsed, runs it, applies the filter, formats records, and slices to `maxSnippets`.
|
|
223
|
+
2. Merges the fresh snippets into the existing set: preserves `shownCount` for matching text, optionally dedups, sorts by `priorityOrder` then by recency, and caps to `injection.maxVisible`.
|
|
224
|
+
3. Atomically writes the cache, then sleeps `injection.refresh` seconds.
|
|
225
|
+
|
|
226
|
+
`stop` / `restart` manage the process; `status` reports whether it's running and lists the current snippets.
|
|
227
|
+
|
|
228
|
+
### Cache file format (`~/.contextspin-cache.json`)
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"updatedAt": "2026-06-17T09:00:00.000Z",
|
|
233
|
+
"snippets": [
|
|
234
|
+
{
|
|
235
|
+
"text": "CI failing: build on main",
|
|
236
|
+
"source": "CI",
|
|
237
|
+
"sourceId": 2,
|
|
238
|
+
"fetchedAt": "2026-06-17T09:00:00.000Z",
|
|
239
|
+
"shownCount": 0
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`shownCount` is incremented by the status-line renderer each time a snippet is displayed; once it reaches `cooldownAfterShown` the snippet is no longer shown.
|
|
246
|
+
|
|
247
|
+
## CLI commands
|
|
248
|
+
|
|
249
|
+
| Command | What it does |
|
|
250
|
+
|---------|--------------|
|
|
251
|
+
| `contextspin setup [--yes]` | Create `~/.contextspin.json` (interactive, or from the bundled example with `--yes` / non-TTY). |
|
|
252
|
+
| `contextspin start` | Start the detached polling daemon. |
|
|
253
|
+
| `contextspin stop` | Stop the daemon. |
|
|
254
|
+
| `contextspin restart` | Stop then start. |
|
|
255
|
+
| `contextspin status` | Show daemon state and the current cached snippets (source, age, shown count). |
|
|
256
|
+
| `contextspin inject [--mode <m>]` | Install the injector. `<m>` overrides `injection.mode` (`statusline` / `patcher` / `both`). |
|
|
257
|
+
| `contextspin uninject [--mode <m>]` | Reverse the injector. |
|
|
258
|
+
| `contextspin` *(no subcommand)* | `setup` if unconfigured, otherwise `start` then `inject`. |
|
|
259
|
+
|
|
260
|
+
## High-impact snippets
|
|
261
|
+
|
|
262
|
+
Three tiers, by how time-sensitive they are.
|
|
263
|
+
|
|
264
|
+
### Tier 1 — time-sensitive (act in minutes)
|
|
265
|
+
|
|
266
|
+
```json
|
|
267
|
+
{ "type": "cli", "command": "gh run list --json status,name,headBranch --limit 5",
|
|
268
|
+
"filter": "{{ status }} == failure",
|
|
269
|
+
"format": "CI failing: {{ name }} on {{ headBranch }}", "label": "CI", "cooldown": 60, "maxSnippets": 2 }
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{ "type": "http", "url": "https://grafana.example.com/api/datasources/proxy/1/query?q=incidents",
|
|
274
|
+
"headers": { "Authorization": "Bearer {{ env.GRAFANA_TOKEN }}" },
|
|
275
|
+
"jq": ".results[0].value", "format": "Grafana: {{ value }}", "label": "Grafana", "cooldown": 30, "maxSnippets": 1 }
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Tier 2 — ambient ops (good to know)
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
{ "type": "mcp", "tool": "slack_search_public", "args": { "query": "mentions:me is:unread" },
|
|
282
|
+
"format": "Slack: {{ text }}", "label": "Slack", "cooldown": 300, "maxSnippets": 2 }
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{ "type": "mcp", "tool": "notion-search", "args": { "query": "assigned:me status:open" },
|
|
287
|
+
"format": "Notion: {{ text }}", "label": "Notion", "cooldown": 300, "maxSnippets": 2 }
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Tier 3 — work queue (your to-do)
|
|
291
|
+
|
|
292
|
+
```json
|
|
293
|
+
{ "type": "cli", "command": "gh pr list --review-requested @me --json title,number --limit 3",
|
|
294
|
+
"format": "PR #{{ number }} needs your review: {{ title }}", "label": "GitHub", "cooldown": 120, "maxSnippets": 3 }
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Limitations
|
|
298
|
+
|
|
299
|
+
- **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 in Stage 1 — use a `cli` or `http` source instead. Plugin / managed scopes are ignored.
|
|
300
|
+
- **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.
|
|
301
|
+
- **The status line shows one rotating snippet** at a time, honoring `cooldownAfterShown` so the same item doesn't repeat indefinitely.
|
|
302
|
+
- **The patcher is experimental** and is **overwritten by every Claude Code update**. Treat it as best-effort; the statusline mode is the supported path.
|
|
303
|
+
|
|
304
|
+
## Roadmap
|
|
305
|
+
|
|
306
|
+
- **Stage 1 (now):** stdio MCP / CLI / HTTP sources, polling daemon + cache, statusline injection, experimental binary patcher, the CLI above.
|
|
307
|
+
- **Stage 2 (polish):** quality-of-life improvements — better source discovery, richer setup wizard, more diagnostics.
|
|
308
|
+
- **Stage 3 (`.plugin`):** package ContextSpin as a first-class Claude Code plugin.
|
|
309
|
+
|
|
310
|
+
## References
|
|
311
|
+
|
|
312
|
+
- claude-depester (patcher inspiration): https://github.com/ominiverdi/claude-depester
|
|
313
|
+
- Claude Code status line docs: https://code.claude.com/docs/en/statusline
|
|
314
|
+
- 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)
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
MIT. See [LICENSE](./LICENSE).
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "contextspin",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"contextspin": "src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node --test",
|
|
14
|
+
"start": "node src/cli.js",
|
|
15
|
+
"daemon": "node src/daemon-entry.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"commander": "^12.1.0"
|
|
19
|
+
},
|
|
20
|
+
"optionalDependencies": {
|
|
21
|
+
"node-lief": "^1.0.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"claude-code",
|
|
25
|
+
"claude",
|
|
26
|
+
"statusline",
|
|
27
|
+
"spinner",
|
|
28
|
+
"mcp",
|
|
29
|
+
"devtools",
|
|
30
|
+
"cli",
|
|
31
|
+
"productivity"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"files": [
|
|
35
|
+
"src",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE",
|
|
38
|
+
".contextspin.example.json"
|
|
39
|
+
]
|
|
40
|
+
}
|