lumira 1.7.0 → 1.8.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.
- package/README.md +2 -1
- package/dist/config.js +1 -0
- package/dist/parsers/gsd.js +137 -16
- package/dist/parsers/transcript.js +3 -1
- package/dist/render/line2.js +6 -1
- package/dist/render/line4.js +13 -4
- package/dist/render/powerline-line2.js +6 -1
- package/dist/types.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Interactive wizard — preset, theme, icons — previewed live before write.
|
|
|
26
26
|
|
|
27
27
|
> 3,400+ monthly downloads, zero marketing. Try it for one session — `npx lumira install`.
|
|
28
28
|
|
|
29
|
-
> **What's new in v1.
|
|
29
|
+
> **What's new in v1.8.1:** the GSD widget now mirrors 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. GSD support is on by default and self-gates (no GSD project → nothing renders). 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).
|
|
30
30
|
|
|
31
31
|
## Table of contents
|
|
32
32
|
|
|
@@ -288,6 +288,7 @@ Create `~/.config/lumira/config.json`:
|
|
|
288
288
|
"linesChanged": true,
|
|
289
289
|
"memory": true,
|
|
290
290
|
"agents": true,
|
|
291
|
+
"compactionCount": true,
|
|
291
292
|
"health": false,
|
|
292
293
|
"contextWarningThreshold": 70,
|
|
293
294
|
"contextCriticalThreshold": 85
|
package/dist/config.js
CHANGED
package/dist/parsers/gsd.js
CHANGED
|
@@ -14,13 +14,15 @@ export function parseStateMd(content) {
|
|
|
14
14
|
const state = {};
|
|
15
15
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
16
16
|
if (fmMatch) {
|
|
17
|
-
|
|
17
|
+
const fmText = fmMatch[1];
|
|
18
|
+
// Parse simple scalar fields
|
|
19
|
+
for (const line of fmText.split('\n')) {
|
|
18
20
|
const m = line.match(/^(\w+):\s*(.+)/);
|
|
19
21
|
if (!m)
|
|
20
22
|
continue;
|
|
21
23
|
const [, key, val] = m;
|
|
22
24
|
const v = val.trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
23
|
-
if (v === 'null')
|
|
25
|
+
if (v === 'null' || v === '')
|
|
24
26
|
continue;
|
|
25
27
|
if (key === 'status')
|
|
26
28
|
state.status = v;
|
|
@@ -28,6 +30,51 @@ export function parseStateMd(content) {
|
|
|
28
30
|
state.milestone = v;
|
|
29
31
|
else if (key === 'milestone_name')
|
|
30
32
|
state.milestoneName = v;
|
|
33
|
+
else if (key === 'active_phase')
|
|
34
|
+
state.activePhase = v;
|
|
35
|
+
else if (key === 'next_action')
|
|
36
|
+
state.nextAction = v;
|
|
37
|
+
}
|
|
38
|
+
// Parse next_phases: flow form [a, b]
|
|
39
|
+
const flowMatch = fmText.match(/^next_phases:\s*\[([^\]]*)\]/m);
|
|
40
|
+
if (flowMatch && flowMatch[1]) {
|
|
41
|
+
const items = flowMatch[1].split(',').map(s => {
|
|
42
|
+
const trimmed = s.trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
43
|
+
return trimmed;
|
|
44
|
+
}).filter(s => s.length > 0);
|
|
45
|
+
if (items.length > 0)
|
|
46
|
+
state.nextPhases = items;
|
|
47
|
+
}
|
|
48
|
+
// Parse next_phases: block-list form
|
|
49
|
+
if (!state.nextPhases) {
|
|
50
|
+
const blockMatch = fmText.match(/^next_phases:\s*\n((?:[ \t]*-[ \t]*[^\n]+\n?)*)/m);
|
|
51
|
+
if (blockMatch && blockMatch[1]) {
|
|
52
|
+
const items = [];
|
|
53
|
+
for (const itemLine of blockMatch[1].split('\n')) {
|
|
54
|
+
const itemM = itemLine.match(/^[ \t]*-[ \t]*(.+)/);
|
|
55
|
+
if (itemM) {
|
|
56
|
+
const itemVal = itemM[1].trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
57
|
+
if (itemVal)
|
|
58
|
+
items.push(itemVal);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (items.length > 0)
|
|
62
|
+
state.nextPhases = items;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Parse progress block
|
|
66
|
+
const progressMatch = fmText.match(/^progress:\s*\n((?:[ \t]+\w+:.+\n?)+)/m);
|
|
67
|
+
if (progressMatch && progressMatch[1]) {
|
|
68
|
+
const progressText = progressMatch[1];
|
|
69
|
+
const completedM = progressText.match(/completed_phases:\s*(\d+)/);
|
|
70
|
+
if (completedM)
|
|
71
|
+
state.completedPhases = completedM[1];
|
|
72
|
+
const totalM = progressText.match(/total_phases:\s*(\d+)/);
|
|
73
|
+
if (totalM)
|
|
74
|
+
state.totalPhases = totalM[1];
|
|
75
|
+
const percentM = progressText.match(/percent:\s*(\d+)/);
|
|
76
|
+
if (percentM)
|
|
77
|
+
state.percent = percentM[1];
|
|
31
78
|
}
|
|
32
79
|
}
|
|
33
80
|
const phaseMatch = content.match(/^Phase:\s*(\d+)\s+of\s+(\d+)(?:\s+\(([^)]+)\))?/m);
|
|
@@ -51,6 +98,24 @@ export function parseStateMd(content) {
|
|
|
51
98
|
}
|
|
52
99
|
return state;
|
|
53
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Render a 10-segment progress bar: `█████░░░░░ 50%`.
|
|
103
|
+
*
|
|
104
|
+
* INTENTIONAL deviation from GSD's own statusline (gsd-statusline.js wraps the
|
|
105
|
+
* bar in brackets — `[█████░░░░░] 50%`). lumira drops the brackets so the
|
|
106
|
+
* milestone bar reads as distinct from the line-2 context bar. Do NOT re-add
|
|
107
|
+
* brackets when re-syncing GSD's format — this is a deliberate design choice.
|
|
108
|
+
*/
|
|
109
|
+
function renderProgressBar(percent) {
|
|
110
|
+
if (percent === undefined || percent === null)
|
|
111
|
+
return '';
|
|
112
|
+
const pct = Math.max(0, Math.min(100, parseInt(String(percent), 10)));
|
|
113
|
+
if (isNaN(pct))
|
|
114
|
+
return '';
|
|
115
|
+
const filled = Math.floor(pct / 10);
|
|
116
|
+
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
|
|
117
|
+
return `${bar} ${pct}%`;
|
|
118
|
+
}
|
|
54
119
|
/** Walk up from `cwd` looking for `.planning/STATE.md`; stop at home or filesystem root. */
|
|
55
120
|
export function findStateMd(cwd) {
|
|
56
121
|
const home = homedir();
|
|
@@ -69,27 +134,64 @@ export function findStateMd(cwd) {
|
|
|
69
134
|
/** Format a GSD state into a compact status string: `milestone · status · phase`. */
|
|
70
135
|
function formatState(s) {
|
|
71
136
|
const parts = [];
|
|
137
|
+
// Milestone segment with optional progress bar
|
|
72
138
|
if (s.milestone || s.milestoneName) {
|
|
73
139
|
const ver = s.milestone ?? '';
|
|
74
140
|
const name = s.milestoneName && s.milestoneName !== 'milestone' ? s.milestoneName : '';
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
141
|
+
const bar = renderProgressBar(s.percent);
|
|
142
|
+
const msParts = [ver, name, bar].filter(Boolean);
|
|
143
|
+
if (msParts.length > 0)
|
|
144
|
+
parts.push(msParts.join(' '));
|
|
145
|
+
}
|
|
146
|
+
// Scene selection: activePhase → nextAction → milestone-complete → default
|
|
147
|
+
const phasesStr = s.nextPhases?.length ? s.nextPhases.join('/') : null;
|
|
148
|
+
if (s.activePhase) {
|
|
149
|
+
// Scene 1: activePhase (with optional status)
|
|
150
|
+
parts.push(s.status ? `Phase ${s.activePhase} ${s.status}` : `Phase ${s.activePhase}`);
|
|
78
151
|
}
|
|
79
|
-
if (s.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
152
|
+
else if (s.nextAction && phasesStr) {
|
|
153
|
+
// Scene 2: nextAction + phases when idle
|
|
154
|
+
parts.push(`next ${s.nextAction} ${phasesStr}`);
|
|
155
|
+
}
|
|
156
|
+
else if (Number(s.percent) === 100 || (s.completedPhases && s.totalPhases && s.completedPhases === s.totalPhases)) {
|
|
157
|
+
// Scene 3: milestone complete
|
|
158
|
+
parts.push('milestone complete');
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Scene 4 (default): preserve existing behavior
|
|
162
|
+
if (s.status)
|
|
163
|
+
parts.push(s.status);
|
|
164
|
+
if (s.phaseNum && s.phaseTotal) {
|
|
165
|
+
const phase = s.phaseName ? `${s.phaseName} (${s.phaseNum}/${s.phaseTotal})` : `ph ${s.phaseNum}/${s.phaseTotal}`;
|
|
166
|
+
parts.push(phase);
|
|
167
|
+
}
|
|
84
168
|
}
|
|
85
169
|
return parts.join(' · ');
|
|
86
170
|
}
|
|
171
|
+
/** Compare two semver versions. Returns 1 if a > b, -1 if a < b, 0 if equal. */
|
|
172
|
+
function semverCompare(a, b) {
|
|
173
|
+
const parseVer = (v) => {
|
|
174
|
+
const parts = v.replace(/^v/, '').split('.').map(p => parseInt(p, 10));
|
|
175
|
+
return { major: parts[0] ?? 0, minor: parts[1] ?? 0, patch: parts[2] ?? 0 };
|
|
176
|
+
};
|
|
177
|
+
const av = parseVer(a);
|
|
178
|
+
const bv = parseVer(b);
|
|
179
|
+
if (av.major !== bv.major)
|
|
180
|
+
return av.major > bv.major ? 1 : -1;
|
|
181
|
+
if (av.minor !== bv.minor)
|
|
182
|
+
return av.minor > bv.minor ? 1 : -1;
|
|
183
|
+
if (av.patch !== bv.patch)
|
|
184
|
+
return av.patch > bv.patch ? 1 : -1;
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
87
187
|
/**
|
|
88
188
|
* Read GSD update-check cache. Checks the shared tool-agnostic cache first
|
|
89
189
|
* (`~/.cache/gsd/`, introduced by GSD #1421), then falls back to the legacy
|
|
90
190
|
* per-runtime location (`~/.claude/cache/`) for older GSD installs.
|
|
191
|
+
* Returns update status, stale hooks flag, and dev install flag.
|
|
91
192
|
*/
|
|
92
193
|
function readUpdateCache(sharedCacheFile, legacyCacheFile) {
|
|
194
|
+
const result = { updateAvailable: false, staleHooks: false, devInstall: false };
|
|
93
195
|
const candidates = [
|
|
94
196
|
['shared', sharedCacheFile],
|
|
95
197
|
['legacy', legacyCacheFile],
|
|
@@ -100,19 +202,33 @@ function readUpdateCache(sharedCacheFile, legacyCacheFile) {
|
|
|
100
202
|
try {
|
|
101
203
|
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
102
204
|
if (parsed.update_available) {
|
|
205
|
+
result.updateAvailable = true;
|
|
103
206
|
log('update cache:', source, file);
|
|
104
|
-
return true;
|
|
105
207
|
}
|
|
208
|
+
if (Array.isArray(parsed.stale_hooks) && parsed.stale_hooks.length > 0) {
|
|
209
|
+
result.staleHooks = true;
|
|
210
|
+
}
|
|
211
|
+
// DevInstall: stale_hooks present AND installed is ahead of latest.
|
|
212
|
+
// Guard against an unknown/missing latest explicitly (mirrors GSD's own
|
|
213
|
+
// check) rather than relying on NaN comparison semantics in semverCompare.
|
|
214
|
+
if (result.staleHooks &&
|
|
215
|
+
parsed.installed &&
|
|
216
|
+
parsed.latest &&
|
|
217
|
+
parsed.latest !== 'unknown' &&
|
|
218
|
+
semverCompare(parsed.installed, parsed.latest) > 0) {
|
|
219
|
+
result.devInstall = true;
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
106
222
|
}
|
|
107
223
|
catch { /* ignore malformed */ }
|
|
108
224
|
}
|
|
109
|
-
return
|
|
225
|
+
return result;
|
|
110
226
|
}
|
|
111
227
|
export function getGsdInfo(cwd, opts = {}) {
|
|
112
228
|
const claudeDir = opts.claudeDir ?? process.env['CLAUDE_CONFIG_DIR'] ?? join(homedir(), '.claude');
|
|
113
229
|
const sharedCacheFile = opts.sharedCacheFile ?? join(homedir(), '.cache', 'gsd', 'gsd-update-check.json');
|
|
114
230
|
const legacyCacheFile = join(claudeDir, 'cache', 'gsd-update-check.json');
|
|
115
|
-
const
|
|
231
|
+
const cacheData = readUpdateCache(sharedCacheFile, legacyCacheFile);
|
|
116
232
|
let currentTask;
|
|
117
233
|
const stateFile = findStateMd(cwd || process.cwd());
|
|
118
234
|
if (stateFile) {
|
|
@@ -132,10 +248,15 @@ export function getGsdInfo(cwd, opts = {}) {
|
|
|
132
248
|
else {
|
|
133
249
|
log('no STATE.md found walking up from:', cwd || process.cwd());
|
|
134
250
|
}
|
|
135
|
-
if (!updateAvailable && !currentTask) {
|
|
136
|
-
log('no gsd signal — update=false, task=none (line4 will be empty)');
|
|
251
|
+
if (!cacheData.updateAvailable && !cacheData.staleHooks && !currentTask) {
|
|
252
|
+
log('no gsd signal — update=false, staleHooks=false, task=none (line4 will be empty)');
|
|
137
253
|
return null;
|
|
138
254
|
}
|
|
139
|
-
return {
|
|
255
|
+
return {
|
|
256
|
+
updateAvailable: cacheData.updateAvailable || undefined,
|
|
257
|
+
staleHooks: cacheData.staleHooks || undefined,
|
|
258
|
+
devInstall: cacheData.devInstall || undefined,
|
|
259
|
+
currentTask,
|
|
260
|
+
};
|
|
140
261
|
}
|
|
141
262
|
//# sourceMappingURL=gsd.js.map
|
|
@@ -124,7 +124,7 @@ export function extractToolTarget(toolName, input) {
|
|
|
124
124
|
// same canonicalisation and allow-list semantics. See `path.ts` for caveats
|
|
125
125
|
// about the string-level (non-symlink-following) nature of the check.
|
|
126
126
|
export async function parseTranscript(transcriptPath) {
|
|
127
|
-
const result = { ...EMPTY_TRANSCRIPT, tools: [], agents: [], todos: [] };
|
|
127
|
+
const result = { ...EMPTY_TRANSCRIPT, tools: [], agents: [], todos: [], compactionCount: 0 };
|
|
128
128
|
if (!transcriptPath || !existsSync(transcriptPath)) {
|
|
129
129
|
if (log.enabled)
|
|
130
130
|
log('skip — transcript path missing or nonexistent:', transcriptPath || '(empty)');
|
|
@@ -185,6 +185,8 @@ export async function parseTranscript(transcriptPath) {
|
|
|
185
185
|
const entry = JSON.parse(line);
|
|
186
186
|
if (!result.sessionStart && entry.timestamp)
|
|
187
187
|
result.sessionStart = new Date(entry.timestamp);
|
|
188
|
+
if (entry.type === 'system' && entry.subtype === 'compact_boundary')
|
|
189
|
+
result.compactionCount++;
|
|
188
190
|
const effortMatch = Array.isArray(entry.message?.content)
|
|
189
191
|
? entry.message.content
|
|
190
192
|
.filter((b) => b.type === 'text')
|
package/dist/render/line2.js
CHANGED
|
@@ -25,7 +25,7 @@ export function formatCountdown(resetsAt) {
|
|
|
25
25
|
return `${s}s`;
|
|
26
26
|
}
|
|
27
27
|
export function renderLine2(ctx, c) {
|
|
28
|
-
const { input, tokenSpeed, transcript: { thinkingEffort }, config: { display }, cols, memory, mcp, icons } = ctx;
|
|
28
|
+
const { input, tokenSpeed, transcript: { thinkingEffort, compactionCount }, config: { display }, cols, memory, mcp, icons } = ctx;
|
|
29
29
|
const leftParts = [];
|
|
30
30
|
const rightParts = [];
|
|
31
31
|
// Track context slots pushed so critical rate-limit segments anchor after
|
|
@@ -56,6 +56,11 @@ export function renderLine2(ctx, c) {
|
|
|
56
56
|
contextSlotCount++;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
// Compaction counter — how many times this session has been compacted.
|
|
60
|
+
// Self-gating: renders nothing at count 0. Paired with the context bar.
|
|
61
|
+
if (display.compactionCount && compactionCount > 0) {
|
|
62
|
+
leftParts.push(c.dim(`⊙ ${compactionCount}`));
|
|
63
|
+
}
|
|
59
64
|
// Tokens
|
|
60
65
|
if (display.tokens) {
|
|
61
66
|
const inTokens = input.tokens.input;
|
package/dist/render/line4.js
CHANGED
|
@@ -3,14 +3,23 @@ import { getCustomCommandsForLine, renderCustomCommand } from './shared.js';
|
|
|
3
3
|
export function renderLine4(ctx, c) {
|
|
4
4
|
const { gsd, icons } = ctx;
|
|
5
5
|
const parts = [];
|
|
6
|
-
// GSD widget — only emit when GSD has something to display.
|
|
7
|
-
|
|
6
|
+
// GSD widget — only emit when GSD has something to display. Text and glyphs
|
|
7
|
+
// mirror GSD's own statusline (gsd-statusline.js) so the integration reads
|
|
8
|
+
// identically. The update/stale-hooks indicators render even without a
|
|
9
|
+
// current task, so a GSD update is visible in any project (gated only on the
|
|
10
|
+
// update-check cache, not on being inside a GSD project).
|
|
11
|
+
if (gsd && (gsd.currentTask || gsd.updateAvailable || gsd.staleHooks)) {
|
|
8
12
|
parts.push(c.dim('GSD'));
|
|
9
13
|
if (gsd.currentTask) {
|
|
10
|
-
parts.push(c.bold(`${icons.hammer} ${truncField(gsd.currentTask,
|
|
14
|
+
parts.push(c.bold(`${icons.hammer} ${truncField(gsd.currentTask, 60)}`));
|
|
11
15
|
}
|
|
12
16
|
if (gsd.updateAvailable) {
|
|
13
|
-
parts.push(c.yellow(
|
|
17
|
+
parts.push(c.yellow('⬆ /gsd:update'));
|
|
18
|
+
}
|
|
19
|
+
if (gsd.staleHooks) {
|
|
20
|
+
parts.push(gsd.devInstall
|
|
21
|
+
? c.yellow('⚠ dev install — re-run installer to sync hooks')
|
|
22
|
+
: c.red('⚠ stale hooks — run /gsd:update'));
|
|
14
23
|
}
|
|
15
24
|
}
|
|
16
25
|
// Custom commands (issue #143 phase 3) — line 4 is the lowest-priority line
|
|
@@ -44,7 +44,7 @@ function getApiLatencyBg(pct, palette) {
|
|
|
44
44
|
// urgency channels the user actually needs at a glance. Decision rationale
|
|
45
45
|
// recorded against PR #47.
|
|
46
46
|
function buildSegments(ctx, palette, c) {
|
|
47
|
-
const { input, config: { display }, icons, mcp, transcript: { thinkingEffort } } = ctx;
|
|
47
|
+
const { input, config: { display }, icons, mcp, transcript: { thinkingEffort, compactionCount } } = ctx;
|
|
48
48
|
const segments = [];
|
|
49
49
|
// Context bar — always highest priority. plain=true so the bar cells inherit
|
|
50
50
|
// the powerline segment bg; only %/icon/hint emit color escapes.
|
|
@@ -72,6 +72,11 @@ function buildSegments(ctx, palette, c) {
|
|
|
72
72
|
segments.push({ text: `${formatTokens(used)}/${formatTokens(capacity)}`, bg: palette.dirBg, fg: palette.fg, priority: 90 });
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
// Compaction counter — self-gating at 0; priority 88 keeps it adjacent to
|
|
76
|
+
// the context family (contextBar=100, contextTokens=90).
|
|
77
|
+
if (display.compactionCount && compactionCount > 0) {
|
|
78
|
+
segments.push({ text: `⊙ ${compactionCount}`, bg: palette.dirBg, fg: palette.fg, priority: 88 });
|
|
79
|
+
}
|
|
75
80
|
// 7d projection — computed once, surfaced inside the 7d segment when the
|
|
76
81
|
// badge is visible (≥50%), or as a standalone segment when it isn't. Mirrors
|
|
77
82
|
// classic line2: badge filter hides noise, projection surfaces signal.
|
package/dist/types.js
CHANGED
|
@@ -11,6 +11,7 @@ export const EMPTY_TRANSCRIPT = Object.freeze({
|
|
|
11
11
|
todos: Object.freeze([]),
|
|
12
12
|
thinkingEffort: '',
|
|
13
13
|
sessionStart: null,
|
|
14
|
+
compactionCount: 0,
|
|
14
15
|
});
|
|
15
16
|
/** Hard cap on per-command wall time (ms). */
|
|
16
17
|
export const CUSTOM_COMMAND_MAX_TIMEOUT_MS = 2000;
|
|
@@ -106,12 +107,17 @@ export const DEFAULT_DISPLAY = {
|
|
|
106
107
|
apiLatency: true,
|
|
107
108
|
addedDirs: true,
|
|
108
109
|
worktreeBreadcrumb: true,
|
|
110
|
+
compactionCount: true,
|
|
109
111
|
contextWarningThreshold: DEFAULT_CONTEXT_WARNING_THRESHOLD,
|
|
110
112
|
contextCriticalThreshold: DEFAULT_CONTEXT_CRITICAL_THRESHOLD,
|
|
111
113
|
};
|
|
112
114
|
export const DEFAULT_CONFIG = {
|
|
113
115
|
layout: 'auto',
|
|
114
|
-
|
|
116
|
+
// GSD on by default, mirroring GSD's own always-on statusline. Self-gates to
|
|
117
|
+
// nothing when there's no .planning/STATE.md and no update-check cache, so
|
|
118
|
+
// non-GSD users see no extra line and pay only a few cheap existsSync checks.
|
|
119
|
+
// Minimal/singleline returns early (renderMinimal) and never reaches line 4.
|
|
120
|
+
gsd: true,
|
|
115
121
|
display: { ...DEFAULT_DISPLAY },
|
|
116
122
|
colors: { mode: 'auto' },
|
|
117
123
|
customCommands: { enabled: false, commands: [] },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lumira",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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",
|