lumira 1.8.2 → 1.9.1

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.
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "lumira",
3
+ "owner": {
4
+ "name": "Carlos Cativo"
5
+ },
6
+ "metadata": {
7
+ "description": "Real-time statusline HUD for Claude Code and Qwen Code — analytics, quota projection, themes, powerline",
8
+ "version": "1.9.0"
9
+ },
10
+ "plugins": [
11
+ {
12
+ "name": "lumira",
13
+ "source": "./",
14
+ "description": "Real-time statusline HUD for Claude Code and Qwen Code. Session analytics, API latency widget, 7-day quota projection, auto-compact warnings, 7 themes, powerline support. Zero runtime dependencies. Run /lumira:setup after install to activate.",
15
+ "version": "1.9.0",
16
+ "author": {
17
+ "name": "Carlos Cativo"
18
+ },
19
+ "homepage": "https://github.com/cativo23/lumira#readme",
20
+ "repository": "https://github.com/cativo23/lumira",
21
+ "license": "MIT",
22
+ "keywords": [
23
+ "statusline",
24
+ "hud",
25
+ "claude-code",
26
+ "qwen-code",
27
+ "analytics",
28
+ "metrics",
29
+ "terminal",
30
+ "powerline",
31
+ "themes"
32
+ ]
33
+ }
34
+ ],
35
+ "version": "1.9.0"
36
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "lumira",
3
+ "version": "1.9.0",
4
+ "description": "Real-time statusline HUD for Claude Code and Qwen Code — session analytics, API latency, 7-day quota projection, auto-compact warnings, 7 themes, powerline. Zero runtime deps.",
5
+ "author": {
6
+ "name": "Carlos Cativo"
7
+ },
8
+ "repository": "https://github.com/cativo23/lumira",
9
+ "homepage": "https://github.com/cativo23/lumira#readme",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "claude-code",
13
+ "qwen-code",
14
+ "statusline",
15
+ "hud",
16
+ "terminal",
17
+ "analytics",
18
+ "session",
19
+ "metrics",
20
+ "nerd-font",
21
+ "powerline",
22
+ "themes"
23
+ ],
24
+ "skills": "./skills/"
25
+ }
package/README.md CHANGED
@@ -8,6 +8,17 @@ Real-time statusline plugin for [Claude Code](https://code.claude.com) and Qwen
8
8
 
9
9
  ## Quick start
10
10
 
11
+ **Via Claude Code plugin (recommended):**
12
+
13
+ ```
14
+ /plugin marketplace add cativo23/lumira
15
+ /lumira:setup
16
+ ```
17
+
18
+ No npm required. The `/lumira:setup` skill writes `statusLine.command` automatically. Restart Claude Code when done.
19
+
20
+ **Via npm:**
21
+
11
22
  ```bash
12
23
  npx lumira install
13
24
  ```
@@ -24,9 +35,7 @@ Interactive wizard — preset, theme, icons — previewed live before write.
24
35
  ![Claude Code](https://img.shields.io/badge/Claude_Code-compatible-2d3748?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCI+PHBhdGggZD0iTTY0IDEyOEMzNS44IDEyOCAxMyAxMDUuMiAxMyA3N0MxMyA0OC44IDM1LjggMjYgNjQgMjZjMjguMiAwIDUxIDIyLjggNTEgNTFzLTIyLjggNTEtNTEgNTF6IiBmaWxsPSIjMjQyNTJGIi8+PC9zdmc+)
25
36
  ![Qwen Code](https://img.shields.io/badge/Qwen_Code-compatible-6156FF)
26
37
 
27
- > 3,400+ monthly downloads, zero marketing. Try it for one session `npx lumira install`.
28
-
29
- > **What's new in v1.8.2:** the installer now writes a fast per-render command — it runs the compiled `lumira` binary directly (~10× faster than `npx lumira@latest`, which hit the npm registry on every render) and migrates existing setups automatically. v1.8.1 brought the GSD widget to parity with get-shit-done (GSD)'s own statusline — phase/milestone lifecycle, a milestone progress bar, and `⬆ /gsd:update` / `⚠ stale hooks` indicators that show in any project (on by default, self-gating). Earlier releases added the compaction counter `⊙ N` (v1.8.0), added-dirs badge + worktree breadcrumb (v1.7.0), [`lumira stats` CLI](#stats-cli) (v1.5), `API N%` latency widget (v1.4.0), 7-day quota projection (v1.3.0), and the auto-compact proximity glyph ⚠ (v1.4.1).
38
+ > **What's new in v1.9.0:** lumira is now a **Claude Code plugin** — install with `/plugin marketplace add cativo23/lumira`, no npm required. Run `/lumira:setup` to activate. v1.8.2 made the installer write a fast per-render command (~10× faster). v1.8.1 brought the GSD widget to parity with GSD 1.42.3. Earlier: compaction counter `⊙ N` (v1.8.0), added-dirs badge + worktree breadcrumb (v1.7.0), [`lumira stats` CLI](#stats-cli) (v1.5), `API N%` latency widget (v1.4.0), 7-day quota projection (v1.3.0).
30
39
 
31
40
  ## Table of contents
32
41
 
@@ -95,15 +104,26 @@ Inspired by [claude-hud](https://github.com/jarrodwatts/claude-hud); takes a dif
95
104
 
96
105
  ## Install
97
106
 
98
- The wizard at the top is the fastest path. For the long form:
107
+ ### Option 1 Claude Code plugin (recommended)
108
+
109
+ No npm required. Works with the Claude Code plugin marketplace:
110
+
111
+ ```
112
+ /plugin marketplace add cativo23/lumira
113
+ /lumira:setup
114
+ ```
115
+
116
+ `/lumira:setup` finds the cached binary, writes `statusLine.command` to `~/.claude/settings.json`, and creates a default config. Restart Claude Code when done. To customize afterward: `/lumira:lumira`.
117
+
118
+ ### Option 2 — npm
99
119
 
100
120
  ```bash
101
121
  npx lumira install
102
122
  ```
103
123
 
104
- The installer walks you through three choices — **preset** (`full` / `balanced` / `minimal`), **theme**, and **icons** — showing a live preview at each step. Press `Esc` to abort without writing anything. In non-interactive shells (piped stdin, CI), the installer skips the wizard and writes sensible defaults (`preset: balanced`, `icons: nerd`). If Qwen Code is detected (`~/.qwen/` exists), the `/lumira` skill is installed for both CLIs.
124
+ The installer walks you through **preset** (`full` / `balanced` / `minimal`), **theme**, and **icons** — showing a live preview at each step. Press `Esc` to abort without writing anything. In non-interactive shells (piped stdin, CI), the installer skips the wizard and writes sensible defaults (`preset: balanced`, `icons: nerd`). If Qwen Code is detected (`~/.qwen/` exists), the `/lumira` skill is installed for both CLIs.
105
125
 
106
- For the fastest statusline (the command runs on **every** render), the installer offers to install lumira globally so it can invoke the compiled binary directly (`lumira`, ~60ms) instead of `npx` (which is ~10× slower). It also migrates older `npx lumira@latest` setups to the faster form automatically.
126
+ For the fastest statusline (the command runs on **every** render), the installer offers to install lumira globally so it can invoke the compiled binary directly (`lumira`, ~60ms) instead of `npx` (~10× slower). It also migrates older `npx lumira@latest` setups to the faster form automatically.
107
127
 
108
128
  Or install globally:
109
129
 
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { fileURLToPath } from 'node:url';
3
- import { realpathSync } from 'node:fs';
3
+ import { realpathSync, readFileSync } from 'node:fs';
4
4
  import { homedir } from 'node:os';
5
- import { join } from 'node:path';
5
+ import { join, dirname } from 'node:path';
6
6
  import { readStdin as defaultReadStdin, StdinParseError } from './stdin.js';
7
7
  import { parseGitStatus } from './parsers/git.js';
8
8
  import { parseTranscript } from './parsers/transcript.js';
@@ -98,9 +98,49 @@ function isDirectRun() {
98
98
  return false;
99
99
  }
100
100
  }
101
+ function getVersion() {
102
+ try {
103
+ // Assumes this file lives at dist/index.js — one level below package.json.
104
+ const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), '../package.json');
105
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
106
+ return pkg.version ?? 'unknown';
107
+ }
108
+ catch {
109
+ return 'unknown';
110
+ }
111
+ }
112
+ function printHelp() {
113
+ const version = getVersion();
114
+ const usage = `lumira v${version}
115
+
116
+ Usage: lumira [command]
117
+
118
+ Commands:
119
+ install Interactive setup wizard
120
+ uninstall Remove statusline configuration
121
+ stats [path] Show session statistics
122
+ themes Browse and preview themes
123
+ custom Manage custom commands
124
+
125
+ Options:
126
+ --help, -h Show this help
127
+ --version, -v Print version
128
+
129
+ Run lumira with no arguments to render the statusline (requires Claude Code stdin).
130
+ `;
131
+ process.stdout.write(usage);
132
+ }
101
133
  if (isDirectRun()) {
102
134
  const cmd = process.argv[2];
103
- if (cmd === 'install') {
135
+ if (cmd === '--help' || cmd === '-h') {
136
+ printHelp();
137
+ process.exit(0);
138
+ }
139
+ else if (cmd === '--version' || cmd === '-v') {
140
+ process.stdout.write(`${getVersion()}\n`);
141
+ process.exit(0);
142
+ }
143
+ else if (cmd === 'install') {
104
144
  const configPath = join(homedir(), '.config', 'lumira', 'config.json');
105
145
  install({ configPath }).then(o => process.stdout.write(o)).catch(e => process.stderr.write(`Install error: ${e.message}\n`));
106
146
  }
@@ -147,6 +187,10 @@ if (isDirectRun()) {
147
187
  // event loop refed. Reads the spec JSON from its own stdin.
148
188
  runCustomRefreshFromStdin().then(() => process.exit(0)).catch(() => process.exit(0));
149
189
  }
190
+ else if (cmd !== undefined) {
191
+ process.stderr.write(`Unknown command: ${cmd}. Run with --help for usage.\n`);
192
+ process.exit(1);
193
+ }
150
194
  else {
151
195
  main().then(o => process.stdout.write(o)).catch(e => { if (!(e instanceof StdinParseError))
152
196
  process.stderr.write(`Statusline error: ${e.message}\n`); });
package/dist/normalize.js CHANGED
@@ -29,8 +29,12 @@ export function sanitizeTermString(s) {
29
29
  /** Allowed values for the reasoning effort level field (CC ≥ 2.1.x). */
30
30
  const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
31
31
  /**
32
- * Sum all four token categories from `context_window.current_usage` to compute
33
- * a real context usage total (input + output + cache_read + cache_creation).
32
+ * Sum input token categories from `context_window.current_usage` to compute
33
+ * a real context usage total (input + cache_read + cache_creation).
34
+ * Excludes output_tokens: they are per-turn and reset each call, which would
35
+ * cause the context bar to jitter (jump down at the start of every new turn).
36
+ * Context window fill is determined by what was READ from the context, not
37
+ * by how many tokens were output.
34
38
  * Returns undefined when `cu` is absent or not an object shape.
35
39
  */
36
40
  function getRealUsageTotal(cu) {
@@ -38,7 +42,6 @@ function getRealUsageTotal(cu) {
38
42
  return undefined;
39
43
  const obj = cu;
40
44
  const total = (obj.input_tokens ?? 0)
41
- + (obj.output_tokens ?? 0)
42
45
  + (obj.cache_read_input_tokens ?? 0)
43
46
  + (obj.cache_creation_input_tokens ?? 0);
44
47
  return total;
@@ -100,9 +103,9 @@ export function normalize(input) {
100
103
  if (claude) {
101
104
  ({ denominator: cacheTurnDenominator } = getCacheFields(claude.context_window?.current_usage));
102
105
  }
103
- // Real context usage percentage (Claude only): includes output tokens in the numerator,
104
- // unlike the hook-provided `used_percentage` which excludes them. This gives an accurate
105
- // picture of actual context consumption, especially near auto-compact thresholds.
106
+ // Real context usage percentage (Claude only): sums input + cache_read + cache_creation
107
+ // (output_tokens excluded — per-turn, resets each call, causes bar jitter). More stable
108
+ // than the hook-provided `used_percentage` near auto-compact thresholds.
106
109
  let realUsedPercentage;
107
110
  if (claude) {
108
111
  const total = getRealUsageTotal(claude.context_window?.current_usage);
@@ -113,10 +116,25 @@ export function normalize(input) {
113
116
  }
114
117
  // Auto-compact proximity warning: fires when context fill is in the
115
118
  // [threshold-gap, threshold) window. Uses realUsedPercentage when available
116
- // (more accurate; includes output+cache), falls back to usedPercentage for
119
+ // (more accurate; excludes output tokens), falls back to usedPercentage for
117
120
  // legacy payloads. Gated by platform (different thresholds Claude vs Qwen).
121
+ // For claude-code, honors CLAUDE_CODE_AUTO_COMPACT_WINDOW env var — a fill-%
122
+ // threshold (1-100) that mirrors Claude Code's own auto-compact trigger point.
123
+ // Users who changed this setting in Claude Code should set the same value here.
124
+ // Falls back to the hardcoded 80% default when absent or invalid.
118
125
  const effectivePct = realUsedPercentage ?? contextWindow.used_percentage ?? 0;
119
- const platformAutoCompactThreshold = AUTO_COMPACT_THRESHOLD[platform];
126
+ let platformAutoCompactThreshold = AUTO_COMPACT_THRESHOLD[platform];
127
+ if (platform === 'claude-code') {
128
+ const envVal = process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW;
129
+ if (envVal !== undefined) {
130
+ // Use Number() + Number.isInteger() so floats ("75.5") and trailing-junk
131
+ // strings ("80abc") are rejected rather than silently truncated by parseInt.
132
+ const parsed = Number(envVal);
133
+ if (Number.isInteger(parsed) && parsed >= 1 && parsed <= 100) {
134
+ platformAutoCompactThreshold = parsed;
135
+ }
136
+ }
137
+ }
120
138
  const nearAutoCompact = effectivePct >= (platformAutoCompactThreshold - AUTO_COMPACT_WARNING_GAP)
121
139
  && effectivePct < platformAutoCompactThreshold;
122
140
  // Performance (Qwen only)
@@ -185,14 +185,16 @@ function semverCompare(a, b) {
185
185
  return 0;
186
186
  }
187
187
  /**
188
- * Read GSD update-check cache. Checks the shared tool-agnostic cache first
189
- * (`~/.cache/gsd/`, introduced by GSD #1421), then falls back to the legacy
190
- * per-runtime location (`~/.claude/cache/`) for older GSD installs.
188
+ * Read GSD update-check cache. Checks candidates in order first match wins:
189
+ * 1. open-gsd per-package cache (`gsd-update-check-opengsd-gsd-core.json`, gsd-core 1.4.x)
190
+ * 2. Legacy shared tool-agnostic cache (`gsd-update-check.json`, get-shit-done-cc 1.x)
191
+ * 3. Legacy per-runtime location (`~/.claude/cache/`) for older installs
191
192
  * Returns update status, stale hooks flag, and dev install flag.
192
193
  */
193
- function readUpdateCache(sharedCacheFile, legacyCacheFile) {
194
+ function readUpdateCache(openGsdCacheFile, sharedCacheFile, legacyCacheFile) {
194
195
  const result = { updateAvailable: false, staleHooks: false, devInstall: false };
195
196
  const candidates = [
197
+ ['open-gsd', openGsdCacheFile],
196
198
  ['shared', sharedCacheFile],
197
199
  ['legacy', legacyCacheFile],
198
200
  ];
@@ -226,9 +228,11 @@ function readUpdateCache(sharedCacheFile, legacyCacheFile) {
226
228
  }
227
229
  export function getGsdInfo(cwd, opts = {}) {
228
230
  const claudeDir = opts.claudeDir ?? process.env['CLAUDE_CONFIG_DIR'] ?? join(homedir(), '.claude');
229
- const sharedCacheFile = opts.sharedCacheFile ?? join(homedir(), '.cache', 'gsd', 'gsd-update-check.json');
231
+ const gsdCacheDir = join(homedir(), '.cache', 'gsd');
232
+ const openGsdCacheFile = opts.openGsdCacheFile ?? join(gsdCacheDir, 'gsd-update-check-opengsd-gsd-core.json');
233
+ const sharedCacheFile = opts.sharedCacheFile ?? join(gsdCacheDir, 'gsd-update-check.json');
230
234
  const legacyCacheFile = join(claudeDir, 'cache', 'gsd-update-check.json');
231
- const cacheData = readUpdateCache(sharedCacheFile, legacyCacheFile);
235
+ const cacheData = readUpdateCache(openGsdCacheFile, sharedCacheFile, legacyCacheFile);
232
236
  let currentTask;
233
237
  const stateFile = findStateMd(cwd || process.cwd());
234
238
  if (stateFile) {
@@ -53,6 +53,14 @@ export async function aggregateStats(transcriptPath) {
53
53
  throw new Error(`Transcript file not found: ${transcriptPath}`);
54
54
  }
55
55
  const stats = emptyStats();
56
+ // Dedup guard: Claude Code streams one JSONL entry per content block for
57
+ // the same logical message (thinking → text → tool_use all share one
58
+ // message.id). Each entry carries the full usage block for that turn, so
59
+ // naively accumulating every entry inflates tokens/cost by the number of
60
+ // content blocks. Track seen message IDs and skip usage+cost on repeats.
61
+ // Content extraction (tool_use, tool_result) is NOT gated — each block
62
+ // appears exactly once in its own entry and must still be counted.
63
+ const seenMessageIds = new Set();
56
64
  let fileStream = null;
57
65
  try {
58
66
  fileStream = createReadStream(resolved);
@@ -81,10 +89,20 @@ export async function aggregateStats(transcriptPath) {
81
89
  }
82
90
  }
83
91
  const message = (entry.message ?? null);
92
+ // Determine whether this is the first time we're seeing this message.id.
93
+ // Entries without a message.id (e.g. user turns, summary lines) are
94
+ // always treated as first-occurrence so they're never suppressed.
95
+ const messageId = message !== null && typeof message.id === 'string' ? message.id : null;
96
+ const isFirstOccurrence = messageId === null || !seenMessageIds.has(messageId);
97
+ if (messageId !== null)
98
+ seenMessageIds.add(messageId);
84
99
  // Usage block (assistant turns only). The mere presence of a usage
85
100
  // payload flips hasCostData=true even if all counts are zero — see
86
101
  // the "zero-cost-with-usage" test for why this matters.
87
- if (entry.type === 'assistant' && message && typeof message === 'object') {
102
+ //
103
+ // Gated on isFirstOccurrence: duplicate entries for the same message.id
104
+ // carry identical usage blocks; accumulating them inflates counts 2-3×.
105
+ if (isFirstOccurrence && entry.type === 'assistant' && message && typeof message === 'object') {
88
106
  const usage = message.usage;
89
107
  if (usage && typeof usage === 'object') {
90
108
  stats.hasCostData = true;
@@ -104,10 +122,14 @@ export async function aggregateStats(transcriptPath) {
104
122
  // undefined`), not truthiness — Anthropic emits `total_cost_usd: 0` for
105
123
  // fully-cached turns, and a `topCost || msgCost` short-circuit would
106
124
  // incorrectly fall through to the message field in that case.
107
- const topCost = safeNumber(entry.total_cost_usd);
108
- const msgCost = message ? safeNumber(message.total_cost_usd) : 0;
109
- const costContribution = entry.total_cost_usd !== undefined ? topCost : msgCost;
110
- stats.costUsd += costContribution;
125
+ //
126
+ // Also gated on isFirstOccurrence for the same dedup reason as usage above.
127
+ if (isFirstOccurrence) {
128
+ const topCost = safeNumber(entry.total_cost_usd);
129
+ const msgCost = message ? safeNumber(message.total_cost_usd) : 0;
130
+ const costContribution = entry.total_cost_usd !== undefined ? topCost : msgCost;
131
+ stats.costUsd += costContribution;
132
+ }
111
133
  // Tool / agent / error extraction from the message.content array.
112
134
  const content = message?.content;
113
135
  if (!Array.isArray(content))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lumira",
3
- "version": "1.8.2",
3
+ "version": "1.9.1",
4
4
  "description": "Real-time statusline HUD for Claude Code and Qwen Code. Includes session analytics CLI, API latency overhead widget, 7d quota projection, auto-compact proximity warnings, themes, and powerline. Zero deps.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,6 +15,7 @@
15
15
  "test:coverage": "vitest run --coverage",
16
16
  "lint": "tsc --noEmit",
17
17
  "themes:validate": "npm run build && node scripts/validate-themes.mjs",
18
+ "version": "node scripts/bump-plugin-version.mjs",
18
19
  "prepublishOnly": "npm run build && npm run lint && node scripts/validate-themes.mjs && npm run test"
19
20
  },
20
21
  "devDependencies": {
@@ -50,6 +51,7 @@
50
51
  "files": [
51
52
  "dist",
52
53
  "skills",
54
+ ".claude-plugin",
53
55
  "!dist/**/*.map",
54
56
  "!dist/**/*.d.ts"
55
57
  ]
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: setup
3
+ description: Use after installing the lumira plugin to activate the statusline. Writes statusLine.command to ~/.claude/settings.json pointing to the plugin-cached binary. Run this once after /plugin marketplace add cativo23/lumira.
4
+ allowed-tools: Bash, Read, Write
5
+ license: MIT
6
+ ---
7
+
8
+ # /lumira:setup — Activate lumira statusline
9
+
10
+ You activate the **lumira** statusline for Claude Code after plugin installation.
11
+
12
+ ## Workflow
13
+
14
+ 1. **Find install path** — Read `~/.claude/plugins/installed_plugins.json`. Look for any key matching `lumira@*` (the exact marketplace key depends on how the user installed it). Extract `installPath` from the first match.
15
+ 2. **Verify binary** — Check that `<installPath>/dist/index.js` exists.
16
+ 3. **Read settings** — Read `~/.claude/settings.json`. If the file is **missing**, start from `{}`. If it is **present but invalid JSON**, stop and report the parse error — do not overwrite.
17
+ 4. **Check existing** — If `statusLine.command` is already set to any non-empty value, tell the user what's currently set and ask if they want to replace it. Do not proceed until they confirm. If it is empty or absent, skip this step.
18
+ 5. **Write command** — Set `statusLine.command` to `node "<installPath>/dist/index.js"`.
19
+ 6. **Init config** — Only create `~/.config/lumira/config.json` if it does not already exist. Run the following Bash — the `test -f` guard is mandatory, do not skip it:
20
+ ```bash
21
+ if [ ! -f ~/.config/lumira/config.json ]; then
22
+ mkdir -p ~/.config/lumira
23
+ printf '{"preset": "balanced"}\n' > ~/.config/lumira/config.json
24
+ echo "Created default config at ~/.config/lumira/config.json"
25
+ else
26
+ echo "Config already exists at ~/.config/lumira/config.json — not modified"
27
+ fi
28
+ ```
29
+ Never overwrite an existing config under any circumstances.
30
+ 7. **Confirm and instruct** — Tell the user what was written and that they must restart Claude Code.
31
+
32
+ ## Finding the install path
33
+
34
+ ```bash
35
+ cat ~/.claude/plugins/installed_plugins.json
36
+ ```
37
+
38
+ Parse the JSON and find the entry whose key starts with `lumira@`. The `installPath` field is the absolute path to the cached plugin directory.
39
+
40
+ If multiple entries match (unlikely), use the one with the most recent `lastUpdated`.
41
+
42
+ ## settings.json merge rules
43
+
44
+ - Always read the full file first, then patch only `statusLine.command`.
45
+ - Write back the complete merged object — never truncate other fields.
46
+ - If the file is missing, create it with just `{"statusLine": {"command": "<node command>"}}`.
47
+ - If the file is not valid JSON, report the parse error and stop. Do not overwrite.
48
+
49
+ ## Success output
50
+
51
+ After writing, tell the user:
52
+ - The exact command written
53
+ - That they need to **restart Claude Code** for the statusline to appear
54
+ - "Run /lumira:lumira to customize preset, theme, or display widgets"
55
+
56
+ ## Error cases
57
+
58
+ - No `lumira@*` key in installed_plugins.json → "Lumira doesn't appear to be installed as a plugin. Run: `/plugin marketplace add cativo23/lumira` then try again."
59
+ - `dist/index.js` missing at install path → "Plugin installed but dist/ is missing. Reinstall: uninstall lumira then `/plugin marketplace add cativo23/lumira`."
60
+ - settings.json parse error → show the exact error, stop, do not write.
61
+
62
+ ## Language
63
+
64
+ Respond in the user's language. Spanish input → Rioplatense Spanish (voseo: "vos tenés", "reiniciá", "fijate").