contextspin 0.3.0 → 0.4.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/package.json +1 -1
- package/src/config.js +15 -0
- package/src/inject/statusline.js +58 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contextspin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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/config.js
CHANGED
|
@@ -56,6 +56,21 @@ export const CLAUDE_USER_CONFIG_PATH = path.join(HOME, ".claude.json");
|
|
|
56
56
|
/** Suffix appended to a Claude install path to name its patcher backup. */
|
|
57
57
|
export const PATCHER_BACKUP_SUFFIX = ".contextspin.backup";
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Built-in "prefilled" snippet texts. The statusline render script falls back to
|
|
61
|
+
* these (rotating through them) whenever there is no live snippet to show — so
|
|
62
|
+
* the status bar is NEVER empty, even immediately after install before the daemon
|
|
63
|
+
* has fetched anything, or when every source returns nothing. They double as
|
|
64
|
+
* onboarding hints pointing at the next useful thing to configure.
|
|
65
|
+
*/
|
|
66
|
+
export const DEFAULT_SNIPPETS = [
|
|
67
|
+
"✨ ContextSpin is live — real-time context, right in your statusline",
|
|
68
|
+
"📅 Add a calendar source to see your next meeting here",
|
|
69
|
+
"👀 Add a GitHub source to surface PRs awaiting your review",
|
|
70
|
+
"🌤️ Add the weather starter source for local conditions at a glance",
|
|
71
|
+
"🛠️ Run the contextspin-setup skill to wire up more sources",
|
|
72
|
+
];
|
|
73
|
+
|
|
59
74
|
/** Default top-level config sections. */
|
|
60
75
|
export const DEFAULTS = {
|
|
61
76
|
injection: { mode: "statusline", refresh: 30, maxVisible: 5 },
|
package/src/inject/statusline.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
CACHE_PATH,
|
|
33
33
|
CONFIG_PATH,
|
|
34
34
|
CLAUDE_SETTINGS_PATH,
|
|
35
|
+
DEFAULT_SNIPPETS,
|
|
35
36
|
} from "../config.js";
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -72,6 +73,7 @@ function buildRenderScript(cachePath, configPath, prevPath) {
|
|
|
72
73
|
const CACHE = JSON.stringify(cachePath);
|
|
73
74
|
const CONFIG = JSON.stringify(configPath);
|
|
74
75
|
const PREV = JSON.stringify(prevPath);
|
|
76
|
+
const DEFAULTS = JSON.stringify(DEFAULT_SNIPPETS);
|
|
75
77
|
return `// contextspin statusline-render.js (generated) — composes any prior
|
|
76
78
|
// statusline (looked up per-project) with one ContextSpin snippet line. MUST
|
|
77
79
|
// always exit 0 and never lose the prior statusline's output, so the user's
|
|
@@ -82,6 +84,7 @@ import { spawn } from "node:child_process";
|
|
|
82
84
|
const CACHE_PATH = ${CACHE};
|
|
83
85
|
const CONFIG_PATH = ${CONFIG};
|
|
84
86
|
const PREV_STATUSLINE_PATH = ${PREV};
|
|
87
|
+
const DEFAULT_SNIPPETS = ${DEFAULTS};
|
|
85
88
|
|
|
86
89
|
/** Buffer ALL of stdin into a Buffer. Resolves on end/close/error/timeout. */
|
|
87
90
|
function readStdin() {
|
|
@@ -243,16 +246,41 @@ function writeJsonAtomic(filePath, data) {
|
|
|
243
246
|
fs.renameSync(tmp, filePath);
|
|
244
247
|
}
|
|
245
248
|
|
|
246
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* Pick a rotating built-in default snippet text. NEVER exhausts (defaults are
|
|
251
|
+
* always available) — this is the guarantee that the status bar is never empty.
|
|
252
|
+
* Persists a rotating index back into the cache object (caller writes it).
|
|
253
|
+
*/
|
|
254
|
+
function defaultLine(cache) {
|
|
255
|
+
if (!Array.isArray(DEFAULT_SNIPPETS) || DEFAULT_SNIPPETS.length === 0) return "";
|
|
256
|
+
const n = DEFAULT_SNIPPETS.length;
|
|
257
|
+
const idx = Number.isInteger(cache && cache._defaultIndex) ? cache._defaultIndex : 0;
|
|
258
|
+
const text = DEFAULT_SNIPPETS[((idx % n) + n) % n];
|
|
259
|
+
if (cache && typeof cache === "object") {
|
|
260
|
+
cache._defaultIndex = (idx + 1) % n;
|
|
261
|
+
try {
|
|
262
|
+
writeJsonAtomic(CACHE_PATH, cache);
|
|
263
|
+
} catch {
|
|
264
|
+
// best effort — still show the default this render
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return String(text).replace(/\\r?\\n/g, " ");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Compute the ContextSpin snippet line. ALWAYS returns a non-empty string: a
|
|
272
|
+
* live snippet when one is eligible, otherwise a rotating built-in default so the
|
|
273
|
+
* status bar is never blank.
|
|
274
|
+
*/
|
|
247
275
|
function contextSpinLine() {
|
|
248
276
|
let cache;
|
|
249
277
|
try {
|
|
250
278
|
cache = JSON.parse(fs.readFileSync(CACHE_PATH, "utf8"));
|
|
251
279
|
} catch {
|
|
252
|
-
|
|
280
|
+
cache = {};
|
|
253
281
|
}
|
|
254
|
-
|
|
255
|
-
|
|
282
|
+
if (!cache || typeof cache !== "object") cache = {};
|
|
283
|
+
const snippets = Array.isArray(cache.snippets) ? cache.snippets : [];
|
|
256
284
|
|
|
257
285
|
// cooldownAfterShown from config (fallback 3).
|
|
258
286
|
let cooldownAfterShown = 3;
|
|
@@ -267,7 +295,8 @@ function contextSpinLine() {
|
|
|
267
295
|
const eligible = snippets.filter(
|
|
268
296
|
(s) => s && typeof s.text === "string" && (s.shownCount || 0) < cooldownAfterShown
|
|
269
297
|
);
|
|
270
|
-
|
|
298
|
+
// No live snippet to show -> fall back to a rotating built-in default.
|
|
299
|
+
if (eligible.length === 0) return defaultLine(cache);
|
|
271
300
|
|
|
272
301
|
eligible.sort((a, b) => {
|
|
273
302
|
const ca = a.shownCount || 0;
|
|
@@ -289,6 +318,28 @@ function contextSpinLine() {
|
|
|
289
318
|
return String(chosen.text).replace(/\\r?\\n/g, " ");
|
|
290
319
|
}
|
|
291
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Wrap the ContextSpin line in a compact, "boxed" ANSI style — bright italic
|
|
323
|
+
* text between cyan bars — so it stands out from the prior statusline. Honors
|
|
324
|
+
* \`injection.style: false\` in the config to opt out (plain text). Any error
|
|
325
|
+
* falls back to the plain text.
|
|
326
|
+
*/
|
|
327
|
+
function styleLine(text) {
|
|
328
|
+
if (!text) return text;
|
|
329
|
+
let enabled = true;
|
|
330
|
+
try {
|
|
331
|
+
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
|
332
|
+
if (cfg && cfg.injection && cfg.injection.style === false) enabled = false;
|
|
333
|
+
} catch {
|
|
334
|
+
// keep enabled
|
|
335
|
+
}
|
|
336
|
+
if (!enabled) return text;
|
|
337
|
+
const BAR = "\\x1b[36m"; // cyan
|
|
338
|
+
const BODY = "\\x1b[3;96m"; // italic + bright cyan
|
|
339
|
+
const RESET = "\\x1b[0m";
|
|
340
|
+
return BAR + "┃" + RESET + " " + BODY + text + RESET + " " + BAR + "┃" + RESET;
|
|
341
|
+
}
|
|
342
|
+
|
|
292
343
|
/** Write a string to stdout, awaiting the flush callback. */
|
|
293
344
|
function writeOut(text) {
|
|
294
345
|
return new Promise((resolve) => {
|
|
@@ -314,10 +365,10 @@ async function main() {
|
|
|
314
365
|
prevOut = "";
|
|
315
366
|
}
|
|
316
367
|
|
|
317
|
-
// (b) ContextSpin snippet line.
|
|
368
|
+
// (b) ContextSpin snippet line (always non-empty; styled).
|
|
318
369
|
let line = "";
|
|
319
370
|
try {
|
|
320
|
-
line = contextSpinLine();
|
|
371
|
+
line = styleLine(contextSpinLine());
|
|
321
372
|
} catch {
|
|
322
373
|
line = "";
|
|
323
374
|
}
|