contextspin 0.6.2 → 0.6.4
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 +26 -17
- package/package.json +1 -1
- package/src/cli.js +2 -83
- package/src/config.js +8 -2
- package/src/inject/hook.js +105 -0
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# ContextSpin
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
curl -fsSL https://raw.githubusercontent.com/mannutech/contextspin/main/install.sh | bash
|
|
7
|
+
```
|
|
4
8
|
|
|
5
9
|
## Key principle: ContextSpin does NOT fetch data
|
|
6
10
|
|
|
@@ -33,15 +37,14 @@ ContextSpin polls those sources on a schedule, formats whatever they return into
|
|
|
33
37
|
│ read cache
|
|
34
38
|
▼
|
|
35
39
|
┌─────────────────────────────────────────────────────────┐
|
|
36
|
-
│ INJECTOR
|
|
40
|
+
│ INJECTOR (statusline — non-destructive, composed) │
|
|
37
41
|
│ statusline ──► ~/.contextspin/statusline.sh │
|
|
38
|
-
│
|
|
39
|
-
│ patcher ──► rewrites spinner words
|
|
40
|
-
│ (EXPERIMENTAL) │
|
|
42
|
+
│ composed into ~/.claude/settings.json │
|
|
43
|
+
│ patcher ──► rewrites spinner words (EXPERIMENTAL) │
|
|
41
44
|
└───────────────────────────┬─────────────────────────────┘
|
|
42
45
|
│
|
|
43
46
|
▼
|
|
44
|
-
|
|
47
|
+
Claude Code status bar shows one snippet
|
|
45
48
|
```
|
|
46
49
|
|
|
47
50
|
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.
|
|
@@ -170,7 +173,7 @@ ContextSpin reads one JSON file: `~/.contextspin.json` (override with the `CONTE
|
|
|
170
173
|
|
|
171
174
|
| Field | Type | Format / values | Default | Meaning |
|
|
172
175
|
|-------|------|-----------------|---------|---------|
|
|
173
|
-
| `sources` | array |
|
|
176
|
+
| `sources` | array | — | `[]` | Sources to poll. May be empty (the bar then shows the built-in defaults). |
|
|
174
177
|
| `sources[].type` | string | `mcp` \| `cli` \| `http` | — | Source kind. Required. |
|
|
175
178
|
| `sources[].tool` | string | tool name or `mcp__server__tool` | — | Required for `mcp`. |
|
|
176
179
|
| `sources[].command` | string | shell command | — | Required for `cli`. |
|
|
@@ -183,6 +186,7 @@ ContextSpin reads one JSON file: `~/.contextspin.json` (override with the `CONTE
|
|
|
183
186
|
| `injection.mode` | string | `statusline` \| `patcher` \| `both` | `statusline` | How snippets reach the UI. |
|
|
184
187
|
| `injection.refresh` | number | seconds | `30` | Daemon poll interval and status-line refresh interval (seconds). |
|
|
185
188
|
| `injection.maxVisible` | number | count | `5` | Global cap on snippets held in the cache. |
|
|
189
|
+
| `injection.style` | boolean | — | `true` | Render the line in a styled box (cyan bars + italic). Set `false` for plain text. |
|
|
186
190
|
| `snippets.deduplication` | boolean | — | `true` | Drop snippets with duplicate text when merging. |
|
|
187
191
|
| `snippets.cooldownAfterShown` | number | count | `3` | A snippet stops being eligible once shown this many times. |
|
|
188
192
|
| `snippets.priorityOrder` | string[] | source labels | `[]` | Earlier labels sort first (case-insensitive); unlisted sort last. |
|
|
@@ -207,9 +211,9 @@ This is the supported path. It uses Claude Code's official [status line](https:/
|
|
|
207
211
|
|
|
208
212
|
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.
|
|
209
213
|
2. Write `~/.contextspin/statusline.sh` — a `0755` bash wrapper that `exec`s the render script.
|
|
210
|
-
3.
|
|
214
|
+
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.
|
|
211
215
|
|
|
212
|
-
Reverse it with `contextspin uninject` (
|
|
216
|
+
Reverse it with `contextspin uninject` (this scope) or `contextspin uninstall` (every scope it ever wired, plus the hook and daemon).
|
|
213
217
|
|
|
214
218
|
### `patcher` (EXPERIMENTAL — binary patching)
|
|
215
219
|
|
|
@@ -258,13 +262,16 @@ Restore the originals with `contextspin uninject --mode patcher` (or `inject --m
|
|
|
258
262
|
|
|
259
263
|
| Command | What it does |
|
|
260
264
|
|---------|--------------|
|
|
261
|
-
| `contextspin
|
|
265
|
+
| `contextspin install` | **One-shot install:** wire a self-healing SessionStart hook, create the config, wire the statusline, and start the daemon. (This is what the curl script runs.) |
|
|
266
|
+
| `contextspin uninstall` | **Full teardown:** remove the hook, restore your prior statusline in **every** scope it wired, and stop the daemon. |
|
|
267
|
+
| `contextspin setup [--yes]` | Create `~/.contextspin.json` (interactive, or a detected config with `--yes` / non-TTY). |
|
|
262
268
|
| `contextspin start` | Start the detached polling daemon. |
|
|
263
269
|
| `contextspin stop` | Stop the daemon. |
|
|
264
270
|
| `contextspin restart` | Stop then start. |
|
|
265
271
|
| `contextspin status` | Show daemon state and the current cached snippets (source, age, shown count). |
|
|
266
|
-
| `contextspin
|
|
267
|
-
| `contextspin
|
|
272
|
+
| `contextspin ensure` | Idempotent: create config + wire statusline + start daemon (run by the SessionStart hook each session). |
|
|
273
|
+
| `contextspin inject [--mode <m>]` | Install just the injector. `<m>` overrides `injection.mode` (`statusline` / `patcher` / `both`). |
|
|
274
|
+
| `contextspin uninject [--mode <m>]` | Reverse just the injector. |
|
|
268
275
|
| `contextspin` *(no subcommand)* | `setup` if unconfigured, otherwise `start` then `inject`. |
|
|
269
276
|
|
|
270
277
|
## High-impact snippets
|
|
@@ -306,16 +313,18 @@ Three tiers, by how time-sensitive they are.
|
|
|
306
313
|
|
|
307
314
|
## Limitations
|
|
308
315
|
|
|
309
|
-
- **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
|
|
316
|
+
- **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.
|
|
310
317
|
- **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.
|
|
311
318
|
- **The status line shows one rotating snippet** at a time, honoring `cooldownAfterShown` so the same item doesn't repeat indefinitely.
|
|
312
319
|
- **The patcher is experimental** and is **overwritten by every Claude Code update**. Treat it as best-effort; the statusline mode is the supported path.
|
|
313
320
|
|
|
314
|
-
##
|
|
321
|
+
## Zero-config defaults (never an empty bar)
|
|
322
|
+
|
|
323
|
+
A fresh install needs no setup:
|
|
315
324
|
|
|
316
|
-
-
|
|
317
|
-
- **
|
|
318
|
-
- **
|
|
325
|
+
- 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.
|
|
326
|
+
- 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.
|
|
327
|
+
- 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.
|
|
319
328
|
|
|
320
329
|
## References
|
|
321
330
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contextspin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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": {
|
package/src/cli.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
uninstallStatusline,
|
|
31
31
|
uninstallAllStatuslines,
|
|
32
32
|
} from './inject/statusline.js';
|
|
33
|
+
import { addSessionStartHook, removeSessionStartHook } from './inject/hook.js';
|
|
33
34
|
import { installPatcher, restorePatcher } from './inject/patcher.js';
|
|
34
35
|
import { detectSources } from './detect.js';
|
|
35
36
|
|
|
@@ -476,88 +477,6 @@ async function runUninject(opts = {}) {
|
|
|
476
477
|
}
|
|
477
478
|
}
|
|
478
479
|
|
|
479
|
-
/**
|
|
480
|
-
* The SessionStart hook command the `install` flow wires into the user settings
|
|
481
|
-
* so ContextSpin self-heals every session (the curl install replicates what the
|
|
482
|
-
* Claude Code plugin's hook does, without the marketplace). Runs from a neutral
|
|
483
|
-
* dir so npx never resolves a confused local package (Exit 127).
|
|
484
|
-
*/
|
|
485
|
-
const SESSIONSTART_HOOK_CMD =
|
|
486
|
-
'cd /tmp && npx --yes contextspin ensure >/dev/null 2>&1; exit 0';
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Read+parse a JSON file, returning a fallback on any read/parse error.
|
|
490
|
-
* @param {string} filePath
|
|
491
|
-
* @param {*} fallback
|
|
492
|
-
* @returns {*}
|
|
493
|
-
*/
|
|
494
|
-
function readJsonSafeSync(filePath, fallback) {
|
|
495
|
-
try {
|
|
496
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
497
|
-
} catch {
|
|
498
|
-
return fallback;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Whether a SessionStart entry already runs ContextSpin (so install is idempotent).
|
|
504
|
-
* @param {*} entry
|
|
505
|
-
* @returns {boolean}
|
|
506
|
-
*/
|
|
507
|
-
function entryRunsContextspin(entry) {
|
|
508
|
-
return !!(
|
|
509
|
-
entry &&
|
|
510
|
-
Array.isArray(entry.hooks) &&
|
|
511
|
-
entry.hooks.some(
|
|
512
|
-
(h) => h && typeof h.command === 'string' && h.command.includes('contextspin'),
|
|
513
|
-
)
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
/**
|
|
518
|
-
* Wire a ContextSpin SessionStart hook into the user ~/.claude/settings.json so
|
|
519
|
-
* the daemon + statusline self-heal every session. JSON-merge (preserves every
|
|
520
|
-
* other key and any existing hooks). Idempotent.
|
|
521
|
-
* @returns {boolean} true if the hook was added (false if already present).
|
|
522
|
-
*/
|
|
523
|
-
function addSessionStartHook() {
|
|
524
|
-
fs.mkdirSync(path.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
|
|
525
|
-
const settings = readJsonSafeSync(CLAUDE_SETTINGS_PATH, {});
|
|
526
|
-
const obj = settings && typeof settings === 'object' ? settings : {};
|
|
527
|
-
obj.hooks = obj.hooks && typeof obj.hooks === 'object' ? obj.hooks : {};
|
|
528
|
-
const arr = Array.isArray(obj.hooks.SessionStart) ? obj.hooks.SessionStart : [];
|
|
529
|
-
if (arr.some(entryRunsContextspin)) return false;
|
|
530
|
-
arr.push({
|
|
531
|
-
matcher: '',
|
|
532
|
-
hooks: [{ type: 'command', command: SESSIONSTART_HOOK_CMD, timeout: 15 }],
|
|
533
|
-
});
|
|
534
|
-
obj.hooks.SessionStart = arr;
|
|
535
|
-
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(obj, null, 2));
|
|
536
|
-
return true;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* Remove any ContextSpin SessionStart hook from the user settings (best-effort,
|
|
541
|
-
* JSON-merge). Prunes empty containers.
|
|
542
|
-
* @returns {boolean} true if a hook was removed.
|
|
543
|
-
*/
|
|
544
|
-
function removeSessionStartHook() {
|
|
545
|
-
const settings = readJsonSafeSync(CLAUDE_SETTINGS_PATH, null);
|
|
546
|
-
if (!settings || typeof settings !== 'object' || !settings.hooks) return false;
|
|
547
|
-
const arr = Array.isArray(settings.hooks.SessionStart)
|
|
548
|
-
? settings.hooks.SessionStart
|
|
549
|
-
: [];
|
|
550
|
-
const kept = arr.filter((e) => !entryRunsContextspin(e));
|
|
551
|
-
if (kept.length === arr.length) return false;
|
|
552
|
-
if (kept.length > 0) settings.hooks.SessionStart = kept;
|
|
553
|
-
else delete settings.hooks.SessionStart;
|
|
554
|
-
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
555
|
-
delete settings.hooks;
|
|
556
|
-
}
|
|
557
|
-
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
558
|
-
return true;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
480
|
/**
|
|
562
481
|
* One-shot install (the `curl | bash` entrypoint): wire the SessionStart hook so
|
|
563
482
|
* ContextSpin self-heals each session, then run ensure (config + statusline +
|
|
@@ -565,7 +484,7 @@ function removeSessionStartHook() {
|
|
|
565
484
|
* @returns {Promise<void>}
|
|
566
485
|
*/
|
|
567
486
|
async function runInstall() {
|
|
568
|
-
const addedHook = addSessionStartHook();
|
|
487
|
+
const addedHook = addSessionStartHook(readVersion());
|
|
569
488
|
await runEnsure();
|
|
570
489
|
console.log('');
|
|
571
490
|
console.log(
|
package/src/config.js
CHANGED
|
@@ -34,8 +34,14 @@ export const LOG_PATH = path.join(STATE_DIR, "daemon.log");
|
|
|
34
34
|
/** Path to the generated statusline bash wrapper. */
|
|
35
35
|
export const STATUSLINE_SH = path.join(STATE_DIR, "statusline.sh");
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Path to the generated statusline Node render script. Uses the `.mjs` extension
|
|
39
|
+
* so Node treats it as ESM regardless of version — the script lives in STATE_DIR
|
|
40
|
+
* (no package.json), and Node 18 has no automatic ESM detection, so a plain
|
|
41
|
+
* `.js` would fail to parse its `import` statements ("Cannot use import statement
|
|
42
|
+
* outside a module").
|
|
43
|
+
*/
|
|
44
|
+
export const STATUSLINE_JS = path.join(STATE_DIR, "statusline-render.mjs");
|
|
39
45
|
|
|
40
46
|
/**
|
|
41
47
|
* Path to the recorded prior statusLine commands (captured when we wrap an
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// src/inject/hook.js — manage ContextSpin's SessionStart hook in the user
|
|
2
|
+
// ~/.claude/settings.json. Used by `contextspin install` / `uninstall` (the curl
|
|
3
|
+
// flow) so ContextSpin self-heals every session without the plugin/marketplace.
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { CLAUDE_SETTINGS_PATH } from "../config.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read+parse a JSON file, returning a fallback on any read/parse error.
|
|
11
|
+
* @param {string} filePath
|
|
12
|
+
* @param {*} fallback
|
|
13
|
+
* @returns {*}
|
|
14
|
+
*/
|
|
15
|
+
function readJsonSafeSync(filePath, fallback) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
18
|
+
} catch {
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the SessionStart hook command, pinned to an EXACT version (never
|
|
25
|
+
* `@latest` or a range) so a future release never runs without a deliberate
|
|
26
|
+
* re-install. Runs from a neutral dir so npx can't resolve a confused local
|
|
27
|
+
* package (Exit 127).
|
|
28
|
+
* @param {string} version - The exact package version to pin (e.g. "0.6.3").
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
export function sessionStartHookCmd(version) {
|
|
32
|
+
return `cd /tmp && npx --yes contextspin@${version} ensure >/dev/null 2>&1; exit 0`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether a SessionStart entry already runs ContextSpin (any version).
|
|
37
|
+
* @param {*} entry
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
export function entryRunsContextspin(entry) {
|
|
41
|
+
return !!(
|
|
42
|
+
entry &&
|
|
43
|
+
Array.isArray(entry.hooks) &&
|
|
44
|
+
entry.hooks.some(
|
|
45
|
+
(h) => h && typeof h.command === "string" && h.command.includes("contextspin"),
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Upsert the ContextSpin SessionStart hook into the user settings, pinned to
|
|
52
|
+
* `version`. JSON-merge (preserves every other key and any non-ours hooks).
|
|
53
|
+
* Drops any prior ContextSpin entry (e.g. an older pinned version) so
|
|
54
|
+
* re-installing a newer version re-pins cleanly. Idempotent when already current.
|
|
55
|
+
* @param {string} version
|
|
56
|
+
* @param {string} [settingsPath=CLAUDE_SETTINGS_PATH]
|
|
57
|
+
* @returns {boolean} true if the file was changed (added or re-pinned).
|
|
58
|
+
*/
|
|
59
|
+
export function addSessionStartHook(version, settingsPath = CLAUDE_SETTINGS_PATH) {
|
|
60
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
61
|
+
const settings = readJsonSafeSync(settingsPath, {});
|
|
62
|
+
const obj = settings && typeof settings === "object" ? settings : {};
|
|
63
|
+
obj.hooks = obj.hooks && typeof obj.hooks === "object" ? obj.hooks : {};
|
|
64
|
+
const arr = Array.isArray(obj.hooks.SessionStart) ? obj.hooks.SessionStart : [];
|
|
65
|
+
|
|
66
|
+
const desired = sessionStartHookCmd(version);
|
|
67
|
+
const existing = arr.find(entryRunsContextspin);
|
|
68
|
+
const alreadyCurrent =
|
|
69
|
+
existing &&
|
|
70
|
+
Array.isArray(existing.hooks) &&
|
|
71
|
+
existing.hooks.some((h) => h && h.command === desired);
|
|
72
|
+
if (alreadyCurrent) return false;
|
|
73
|
+
|
|
74
|
+
const others = arr.filter((e) => !entryRunsContextspin(e));
|
|
75
|
+
others.push({
|
|
76
|
+
matcher: "",
|
|
77
|
+
hooks: [{ type: "command", command: desired, timeout: 15 }],
|
|
78
|
+
});
|
|
79
|
+
obj.hooks.SessionStart = others;
|
|
80
|
+
fs.writeFileSync(settingsPath, JSON.stringify(obj, null, 2));
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Remove any ContextSpin SessionStart hook from the user settings (JSON-merge,
|
|
86
|
+
* best-effort). Prunes empty containers.
|
|
87
|
+
* @param {string} [settingsPath=CLAUDE_SETTINGS_PATH]
|
|
88
|
+
* @returns {boolean} true if a hook was removed.
|
|
89
|
+
*/
|
|
90
|
+
export function removeSessionStartHook(settingsPath = CLAUDE_SETTINGS_PATH) {
|
|
91
|
+
const settings = readJsonSafeSync(settingsPath, null);
|
|
92
|
+
if (!settings || typeof settings !== "object" || !settings.hooks) return false;
|
|
93
|
+
const arr = Array.isArray(settings.hooks.SessionStart)
|
|
94
|
+
? settings.hooks.SessionStart
|
|
95
|
+
: [];
|
|
96
|
+
const kept = arr.filter((e) => !entryRunsContextspin(e));
|
|
97
|
+
if (kept.length === arr.length) return false;
|
|
98
|
+
if (kept.length > 0) settings.hooks.SessionStart = kept;
|
|
99
|
+
else delete settings.hooks.SessionStart;
|
|
100
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
101
|
+
delete settings.hooks;
|
|
102
|
+
}
|
|
103
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
104
|
+
return true;
|
|
105
|
+
}
|