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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contextspin",
3
- "version": "0.3.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 },
@@ -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
- /** Compute the ContextSpin snippet line (may be ""); bumps shownCount. */
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
- return "";
280
+ cache = {};
253
281
  }
254
- const snippets = Array.isArray(cache && cache.snippets) ? cache.snippets : [];
255
- if (snippets.length === 0) return "";
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
- if (eligible.length === 0) return "";
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
  }