contextspin 0.6.3 → 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/package.json +1 -1
- package/src/cli.js +2 -101
- package/src/config.js +8 -2
- package/src/inject/hook.js +105 -0
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,106 +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).
|
|
483
|
-
*
|
|
484
|
-
* The hook is pinned to the EXACT version doing the install (never `@latest` or a
|
|
485
|
-
* range), so a future release never runs on the user's machine without them
|
|
486
|
-
* deliberately re-installing. Runs from a neutral dir so npx never resolves a
|
|
487
|
-
* confused local package (Exit 127).
|
|
488
|
-
* @returns {string}
|
|
489
|
-
*/
|
|
490
|
-
function sessionStartHookCmd() {
|
|
491
|
-
return `cd /tmp && npx --yes contextspin@${readVersion()} ensure >/dev/null 2>&1; exit 0`;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Read+parse a JSON file, returning a fallback on any read/parse error.
|
|
496
|
-
* @param {string} filePath
|
|
497
|
-
* @param {*} fallback
|
|
498
|
-
* @returns {*}
|
|
499
|
-
*/
|
|
500
|
-
function readJsonSafeSync(filePath, fallback) {
|
|
501
|
-
try {
|
|
502
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
503
|
-
} catch {
|
|
504
|
-
return fallback;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Whether a SessionStart entry already runs ContextSpin (so install is idempotent).
|
|
510
|
-
* @param {*} entry
|
|
511
|
-
* @returns {boolean}
|
|
512
|
-
*/
|
|
513
|
-
function entryRunsContextspin(entry) {
|
|
514
|
-
return !!(
|
|
515
|
-
entry &&
|
|
516
|
-
Array.isArray(entry.hooks) &&
|
|
517
|
-
entry.hooks.some(
|
|
518
|
-
(h) => h && typeof h.command === 'string' && h.command.includes('contextspin'),
|
|
519
|
-
)
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
/**
|
|
524
|
-
* Wire a ContextSpin SessionStart hook into the user ~/.claude/settings.json so
|
|
525
|
-
* the daemon + statusline self-heal every session. JSON-merge (preserves every
|
|
526
|
-
* other key and any existing hooks). Idempotent.
|
|
527
|
-
* @returns {boolean} true if the hook was added (false if already present).
|
|
528
|
-
*/
|
|
529
|
-
function addSessionStartHook() {
|
|
530
|
-
fs.mkdirSync(path.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
|
|
531
|
-
const settings = readJsonSafeSync(CLAUDE_SETTINGS_PATH, {});
|
|
532
|
-
const obj = settings && typeof settings === 'object' ? settings : {};
|
|
533
|
-
obj.hooks = obj.hooks && typeof obj.hooks === 'object' ? obj.hooks : {};
|
|
534
|
-
const arr = Array.isArray(obj.hooks.SessionStart) ? obj.hooks.SessionStart : [];
|
|
535
|
-
|
|
536
|
-
const desired = sessionStartHookCmd();
|
|
537
|
-
const existing = arr.find(entryRunsContextspin);
|
|
538
|
-
const alreadyCurrent =
|
|
539
|
-
existing &&
|
|
540
|
-
Array.isArray(existing.hooks) &&
|
|
541
|
-
existing.hooks.some((h) => h && h.command === desired);
|
|
542
|
-
// Already exactly this command (same pinned version) — nothing to do.
|
|
543
|
-
if (alreadyCurrent) return false;
|
|
544
|
-
|
|
545
|
-
// Upsert: drop any prior ContextSpin entry (e.g. an older pinned version) and
|
|
546
|
-
// add the current one, so re-installing a newer version re-pins cleanly.
|
|
547
|
-
const others = arr.filter((e) => !entryRunsContextspin(e));
|
|
548
|
-
others.push({
|
|
549
|
-
matcher: '',
|
|
550
|
-
hooks: [{ type: 'command', command: desired, timeout: 15 }],
|
|
551
|
-
});
|
|
552
|
-
obj.hooks.SessionStart = others;
|
|
553
|
-
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(obj, null, 2));
|
|
554
|
-
return true;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Remove any ContextSpin SessionStart hook from the user settings (best-effort,
|
|
559
|
-
* JSON-merge). Prunes empty containers.
|
|
560
|
-
* @returns {boolean} true if a hook was removed.
|
|
561
|
-
*/
|
|
562
|
-
function removeSessionStartHook() {
|
|
563
|
-
const settings = readJsonSafeSync(CLAUDE_SETTINGS_PATH, null);
|
|
564
|
-
if (!settings || typeof settings !== 'object' || !settings.hooks) return false;
|
|
565
|
-
const arr = Array.isArray(settings.hooks.SessionStart)
|
|
566
|
-
? settings.hooks.SessionStart
|
|
567
|
-
: [];
|
|
568
|
-
const kept = arr.filter((e) => !entryRunsContextspin(e));
|
|
569
|
-
if (kept.length === arr.length) return false;
|
|
570
|
-
if (kept.length > 0) settings.hooks.SessionStart = kept;
|
|
571
|
-
else delete settings.hooks.SessionStart;
|
|
572
|
-
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
573
|
-
delete settings.hooks;
|
|
574
|
-
}
|
|
575
|
-
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
576
|
-
return true;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
480
|
/**
|
|
580
481
|
* One-shot install (the `curl | bash` entrypoint): wire the SessionStart hook so
|
|
581
482
|
* ContextSpin self-heals each session, then run ensure (config + statusline +
|
|
@@ -583,7 +484,7 @@ function removeSessionStartHook() {
|
|
|
583
484
|
* @returns {Promise<void>}
|
|
584
485
|
*/
|
|
585
486
|
async function runInstall() {
|
|
586
|
-
const addedHook = addSessionStartHook();
|
|
487
|
+
const addedHook = addSessionStartHook(readVersion());
|
|
587
488
|
await runEnsure();
|
|
588
489
|
console.log('');
|
|
589
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
|
+
}
|