lumira 1.4.1 → 1.6.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/README.md +88 -1
- package/dist/commands/custom-refresh.d.ts +38 -0
- package/dist/commands/custom-refresh.js +124 -0
- package/dist/commands/custom-refresh.js.map +1 -0
- package/dist/commands/custom.d.ts +15 -0
- package/dist/commands/custom.js +280 -0
- package/dist/commands/custom.js.map +1 -0
- package/dist/commands/stats.d.ts +71 -0
- package/dist/commands/stats.js +371 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/config.js +150 -6
- package/dist/config.js.map +1 -1
- package/dist/index.js +66 -1
- package/dist/index.js.map +1 -1
- package/dist/installer.js +1 -0
- package/dist/installer.js.map +1 -1
- package/dist/parsers/custom-commands.d.ts +79 -0
- package/dist/parsers/custom-commands.js +232 -0
- package/dist/parsers/custom-commands.js.map +1 -0
- package/dist/parsers/transcript-stats.d.ts +41 -0
- package/dist/parsers/transcript-stats.js +141 -0
- package/dist/parsers/transcript-stats.js.map +1 -0
- package/dist/render/index.js +6 -1
- package/dist/render/index.js.map +1 -1
- package/dist/render/line1.js +9 -1
- package/dist/render/line1.js.map +1 -1
- package/dist/render/line2.js +9 -1
- package/dist/render/line2.js.map +1 -1
- package/dist/render/line3.js +9 -1
- package/dist/render/line3.js.map +1 -1
- package/dist/render/line4.js +20 -9
- package/dist/render/line4.js.map +1 -1
- package/dist/render/minimal.js +8 -2
- package/dist/render/minimal.js.map +1 -1
- package/dist/render/powerline-line1.d.ts +1 -1
- package/dist/render/powerline-line1.js +19 -3
- package/dist/render/powerline-line1.js.map +1 -1
- package/dist/render/powerline-line2.js +10 -1
- package/dist/render/powerline-line2.js.map +1 -1
- package/dist/render/powerline-line3.d.ts +1 -1
- package/dist/render/powerline-line3.js +15 -3
- package/dist/render/powerline-line3.js.map +1 -1
- package/dist/render/shared.d.ts +32 -0
- package/dist/render/shared.js +62 -1
- package/dist/render/shared.js.map +1 -1
- package/dist/types.d.ts +89 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/custom-cache.d.ts +21 -0
- package/dist/utils/custom-cache.js +77 -0
- package/dist/utils/custom-cache.js.map +1 -0
- package/dist/utils/exec-bg.d.ts +50 -0
- package/dist/utils/exec-bg.js +183 -0
- package/dist/utils/exec-bg.js.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -24,7 +24,9 @@ Interactive wizard — preset, theme, icons — previewed live before write.
|
|
|
24
24
|

|
|
25
25
|

|
|
26
26
|
|
|
27
|
-
>
|
|
27
|
+
> 3,400+ monthly downloads, zero marketing. Try it for one session — `npx lumira install`.
|
|
28
|
+
|
|
29
|
+
> **What's new in v1.5:** the [`lumira stats` CLI](#stats-cli) prints post-session analytics (cost, tokens, cache hit, tool frequency, burn rate). Combined with the `API N%` latency widget (v1.4.0), 7-day quota projection warning (v1.3.0), and auto-compact proximity glyph ⚠ (v1.4.1), recent releases add several diagnostic signals to the session.
|
|
28
30
|
|
|
29
31
|
## Table of contents
|
|
30
32
|
|
|
@@ -34,6 +36,7 @@ Interactive wizard — preset, theme, icons — previewed live before write.
|
|
|
34
36
|
- [Install](#install)
|
|
35
37
|
- [Display modes](#display)
|
|
36
38
|
- [Themes](#themes)
|
|
39
|
+
- [Stats CLI](#stats-cli)
|
|
37
40
|
- [Powerline](#powerline)
|
|
38
41
|
- [Configuration](#configuration)
|
|
39
42
|
- [Architecture](#architecture)
|
|
@@ -190,6 +193,28 @@ lumira themes preview --all --powerline # the powerline grid (great for scr
|
|
|
190
193
|
|
|
191
194
|
Adding a theme is a single new file plus a one-line registration. Every PR runs the **WCAG AA contrast guard** — if any powerline cell drops below 4.5:1 against the foreground, CI rejects it. See [CONTRIBUTING.md → Adding a theme](CONTRIBUTING.md#adding-a-theme) for the walkthrough.
|
|
192
195
|
|
|
196
|
+
## Stats CLI
|
|
197
|
+
|
|
198
|
+
`lumira stats` reads a Claude Code or Qwen Code transcript `.jsonl` and prints a one-shot analytics summary — session duration, total cost, token totals, cache hit rate, tool call frequency, and burn rate (`$/h`). Useful for post-session review, scripting, and CI dashboards.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Just works — auto-discovers the newest transcript for the current cwd.
|
|
202
|
+
lumira stats
|
|
203
|
+
# Session: 2h 15m — $4.23 — 156k tokens — 87% cache
|
|
204
|
+
# Tools: Bash×45 Read×32 Write×18 Edit×12 Agent×8
|
|
205
|
+
# Burn: $1.88/h
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Auto-discovery:** with no flags, `lumira stats` derives the Claude Code project slug from `cwd` (`/home/me/proj` → `-home-me-proj`) and reads the newest `.jsonl` under `~/.claude/projects/<slug>/`. If the current directory has no matching project dir, it falls back to the globally most-recently-modified transcript under `~/.claude/projects/` and prints a notice to stderr ("reading most recent session from …") so JSON pipelines on stdout stay clean.
|
|
209
|
+
|
|
210
|
+
**Flags:**
|
|
211
|
+
|
|
212
|
+
- `--session-id <path-or-uuid>` — override auto-discovery. A path (anything containing `/` or ending in `.jsonl`) is used as-is. A bare uuid is resolved first under `~/.claude/projects/<cwd-slug>/<uuid>.jsonl`, then by scanning every project dir for that filename.
|
|
213
|
+
- `--no-color` — strip ANSI escapes (also honored when the `NO_COLOR` env var is set, per [no-color.org](https://no-color.org)).
|
|
214
|
+
- `--json` — emit the raw `SessionStats` object as pretty-printed JSON for `jq` / CI composability.
|
|
215
|
+
|
|
216
|
+
**Qwen Code sessions** are parsed the same way, but cost and burn-rate lines are suppressed when the transcript lacks usage blocks (`hasCostData: false` in the JSON output) — no misleading `$0.00`.
|
|
217
|
+
|
|
193
218
|
## Powerline
|
|
194
219
|
|
|
195
220
|
`style: "powerline"` (or `--powerline`) renders the statusline with colored segment backgrounds and glyph separators inspired by powerline-go / oh-my-posh. Available separator presets via `powerline.style` (or `--powerline-style=<name>`):
|
|
@@ -287,6 +312,68 @@ lumira --icons=nerd|emoji|none # Override icon set
|
|
|
287
312
|
lumira --preset=full|balanced|minimal
|
|
288
313
|
```
|
|
289
314
|
|
|
315
|
+
## Custom Commands
|
|
316
|
+
|
|
317
|
+
User-defined shell commands rendered as statusline segments on any of the 4 lines. Disabled by default.
|
|
318
|
+
|
|
319
|
+
### Enable
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
lumira custom enable
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Configure
|
|
326
|
+
|
|
327
|
+
Add a `customCommands` block to `~/.config/lumira/config.json`:
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
{
|
|
331
|
+
"customCommands": {
|
|
332
|
+
"enabled": true,
|
|
333
|
+
"commands": [
|
|
334
|
+
{
|
|
335
|
+
"id": "git-status",
|
|
336
|
+
"command": ["git", "status", "--short"],
|
|
337
|
+
"label": "",
|
|
338
|
+
"line": 1,
|
|
339
|
+
"refreshMs": 5000,
|
|
340
|
+
"onError": "hide"
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Key fields:**
|
|
348
|
+
|
|
349
|
+
| Field | Description |
|
|
350
|
+
|---|---|
|
|
351
|
+
| `id` | Unique identifier for the command |
|
|
352
|
+
| `command` | Argv array — no shell expansion, pipes, or redirects |
|
|
353
|
+
| `line` | Statusline line to render on (`1`–`4`) |
|
|
354
|
+
| `refreshMs` | Refresh interval in milliseconds (default: `5000`) |
|
|
355
|
+
| `label` | Optional prefix shown before the command output |
|
|
356
|
+
| `color` | Optional color override for the segment |
|
|
357
|
+
| `onError` | What to show on non-zero exit: `hide` (default), `placeholder`, `output`, or `stale` |
|
|
358
|
+
| `onTimeout` | What to show on timeout: same options as `onError`, defaults to `hide` |
|
|
359
|
+
| `timeoutMs` | Max execution time in ms (clamped to 2000) |
|
|
360
|
+
| `maxBytes` | Max stdout bytes captured (clamped to 4096) |
|
|
361
|
+
| `ansi` | Set `true` to pass through ANSI escape sequences from the command |
|
|
362
|
+
|
|
363
|
+
`command` must be an argv array (`["git", "status", "--short"]`). Shell strings with pipes or redirects are not supported — wrap them in a script if needed.
|
|
364
|
+
|
|
365
|
+
Output is cached with a TTL and refreshed in the background, so the hot render path never blocks on subprocess execution.
|
|
366
|
+
|
|
367
|
+
### CLI subcommands
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
lumira custom list # list configured commands and their status
|
|
371
|
+
lumira custom enable # enable the custom commands feature
|
|
372
|
+
lumira custom disable # disable the custom commands feature
|
|
373
|
+
lumira custom test <id> # run a command immediately and print its output
|
|
374
|
+
lumira custom logs # show recent execution logs
|
|
375
|
+
```
|
|
376
|
+
|
|
290
377
|
## Architecture
|
|
291
378
|
|
|
292
379
|
```text
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helper for the Custom Command widget (#143). Runs ONE custom
|
|
3
|
+
* command and writes the result into the cache file. Intended to be
|
|
4
|
+
* launched by the renderer as a detached child process so the renderer
|
|
5
|
+
* itself can exit immediately without waiting on the spawned command
|
|
6
|
+
* (B1: the original "fire and forget" via void async-IIFE still kept
|
|
7
|
+
* the Node event loop refed because data listeners hold stdin/stdout
|
|
8
|
+
* streams open until child exit + cache write).
|
|
9
|
+
*
|
|
10
|
+
* Wire protocol: a single JSON object on stdin describing the spawn
|
|
11
|
+
* spec, cache path, and (optional) stdin envelope to forward to the
|
|
12
|
+
* user command:
|
|
13
|
+
*
|
|
14
|
+
* {
|
|
15
|
+
* "id": "k8s-context",
|
|
16
|
+
* "command": ["kubectl", "config", "current-context"],
|
|
17
|
+
* "timeoutMs": 1500,
|
|
18
|
+
* "maxBytes": 256,
|
|
19
|
+
* "env": { ... }, // optional
|
|
20
|
+
* "cwd": "/abs/path", // optional
|
|
21
|
+
* "onError": "hide", // 'hide' | 'placeholder' | 'output' | 'stale'
|
|
22
|
+
* "cachePath": "/abs/path/to/custom-commands.json",
|
|
23
|
+
* "stdin": "{}" // optional
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* Errors are swallowed — this process must NEVER print to stdout or
|
|
27
|
+
* crash visibly. The renderer doesn't read this process's exit code.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Read the JSON spec from stdin, run the command via execBg, persist the
|
|
31
|
+
* result. Never throws — every failure path returns silently.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runCustomRefresh(stdinPayload: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* CLI entrypoint used by the renderer. Reads stdin to EOF, then runs.
|
|
36
|
+
* Wrapped in a Promise so the caller can await before exiting.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runCustomRefreshFromStdin(): Promise<void>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helper for the Custom Command widget (#143). Runs ONE custom
|
|
3
|
+
* command and writes the result into the cache file. Intended to be
|
|
4
|
+
* launched by the renderer as a detached child process so the renderer
|
|
5
|
+
* itself can exit immediately without waiting on the spawned command
|
|
6
|
+
* (B1: the original "fire and forget" via void async-IIFE still kept
|
|
7
|
+
* the Node event loop refed because data listeners hold stdin/stdout
|
|
8
|
+
* streams open until child exit + cache write).
|
|
9
|
+
*
|
|
10
|
+
* Wire protocol: a single JSON object on stdin describing the spawn
|
|
11
|
+
* spec, cache path, and (optional) stdin envelope to forward to the
|
|
12
|
+
* user command:
|
|
13
|
+
*
|
|
14
|
+
* {
|
|
15
|
+
* "id": "k8s-context",
|
|
16
|
+
* "command": ["kubectl", "config", "current-context"],
|
|
17
|
+
* "timeoutMs": 1500,
|
|
18
|
+
* "maxBytes": 256,
|
|
19
|
+
* "env": { ... }, // optional
|
|
20
|
+
* "cwd": "/abs/path", // optional
|
|
21
|
+
* "onError": "hide", // 'hide' | 'placeholder' | 'output' | 'stale'
|
|
22
|
+
* "cachePath": "/abs/path/to/custom-commands.json",
|
|
23
|
+
* "stdin": "{}" // optional
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* Errors are swallowed — this process must NEVER print to stdout or
|
|
27
|
+
* crash visibly. The renderer doesn't read this process's exit code.
|
|
28
|
+
*/
|
|
29
|
+
import { execBg } from '../utils/exec-bg.js';
|
|
30
|
+
import { isAbsolute } from 'node:path';
|
|
31
|
+
import { readCacheFile, writeCacheFile } from '../utils/custom-cache.js';
|
|
32
|
+
function isValidSpec(raw) {
|
|
33
|
+
if (!raw || typeof raw !== 'object')
|
|
34
|
+
return false;
|
|
35
|
+
const s = raw;
|
|
36
|
+
if (typeof s.id !== 'string' || s.id.length === 0)
|
|
37
|
+
return false;
|
|
38
|
+
if (!Array.isArray(s.command) || s.command.length === 0)
|
|
39
|
+
return false;
|
|
40
|
+
if (!s.command.every((x) => typeof x === 'string' && x.length > 0))
|
|
41
|
+
return false;
|
|
42
|
+
if (typeof s.timeoutMs !== 'number' || !Number.isFinite(s.timeoutMs) || s.timeoutMs > 2000)
|
|
43
|
+
return false;
|
|
44
|
+
if (typeof s.maxBytes !== 'number' || !Number.isFinite(s.maxBytes) || s.maxBytes > 4096)
|
|
45
|
+
return false;
|
|
46
|
+
if (typeof s.onError !== 'string')
|
|
47
|
+
return false;
|
|
48
|
+
if (typeof s.cachePath !== 'string' || s.cachePath.length === 0 || !isAbsolute(s.cachePath))
|
|
49
|
+
return false;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read the JSON spec from stdin, run the command via execBg, persist the
|
|
54
|
+
* result. Never throws — every failure path returns silently.
|
|
55
|
+
*/
|
|
56
|
+
export async function runCustomRefresh(stdinPayload) {
|
|
57
|
+
let spec;
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(stdinPayload);
|
|
60
|
+
if (!isValidSpec(parsed))
|
|
61
|
+
return;
|
|
62
|
+
spec = parsed;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const result = await execBg({
|
|
69
|
+
command: spec.command,
|
|
70
|
+
timeoutMs: spec.timeoutMs,
|
|
71
|
+
maxBytes: spec.maxBytes,
|
|
72
|
+
env: spec.env,
|
|
73
|
+
cwd: spec.cwd,
|
|
74
|
+
stdin: spec.stdin,
|
|
75
|
+
});
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
let entry;
|
|
78
|
+
switch (result.kind) {
|
|
79
|
+
case 'ok':
|
|
80
|
+
entry = { text: result.stdout, capturedAt: now, state: 'ok' };
|
|
81
|
+
break;
|
|
82
|
+
case 'timeout':
|
|
83
|
+
entry = { text: result.stdout, capturedAt: now, state: 'timeout' };
|
|
84
|
+
break;
|
|
85
|
+
case 'nonzero': {
|
|
86
|
+
// For onError:'output' the renderer shows entry.text directly, so
|
|
87
|
+
// store stdout (partial output before the command failed). For all
|
|
88
|
+
// other strategies the text is not displayed — store stdout anyway
|
|
89
|
+
// so the entry is useful if the strategy is later changed.
|
|
90
|
+
entry = { text: result.stdout, capturedAt: now, state: 'nonzero' };
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case 'spawn-error':
|
|
94
|
+
entry = { text: '', capturedAt: now, state: 'nonzero' };
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
const current = readCacheFile(spec.cachePath);
|
|
98
|
+
current[spec.id] = entry;
|
|
99
|
+
writeCacheFile(spec.cachePath, current);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* swallow — helper must never crash visibly */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* CLI entrypoint used by the renderer. Reads stdin to EOF, then runs.
|
|
107
|
+
* Wrapped in a Promise so the caller can await before exiting.
|
|
108
|
+
*/
|
|
109
|
+
export async function runCustomRefreshFromStdin() {
|
|
110
|
+
const chunks = [];
|
|
111
|
+
try {
|
|
112
|
+
await new Promise((resolve) => {
|
|
113
|
+
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
|
114
|
+
process.stdin.on('end', () => resolve());
|
|
115
|
+
process.stdin.on('error', () => resolve());
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const payload = Buffer.concat(chunks).toString('utf8');
|
|
122
|
+
await runCustomRefresh(payload);
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=custom-refresh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom-refresh.js","sourceRoot":"","sources":["../../src/commands/custom-refresh.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type Result = {
|
|
2
|
+
output: string;
|
|
3
|
+
exitCode: number;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Execute `lumira custom [subcommand] [...args]`.
|
|
7
|
+
*
|
|
8
|
+
* argv is the full process.argv; 'custom' starts at argv[2], the subcommand
|
|
9
|
+
* at argv[3], additional arguments from argv[4] onward.
|
|
10
|
+
*
|
|
11
|
+
* Returns `{ output, exitCode }` — the dispatcher writes output to stdout and
|
|
12
|
+
* sets process.exitCode from the returned value.
|
|
13
|
+
*/
|
|
14
|
+
export declare function runCustomCommand(argv: string[]): Promise<Result>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lumira custom` subcommand (issue #143 phase 4).
|
|
3
|
+
*
|
|
4
|
+
* Provides a CLI interface for managing the custom commands feature:
|
|
5
|
+
*
|
|
6
|
+
* lumira custom list List configured commands from config file
|
|
7
|
+
* lumira custom enable Set enabled:true in config file
|
|
8
|
+
* lumira custom disable Set enabled:false in config file
|
|
9
|
+
* lumira custom test <id> Run a command once, print output + timing
|
|
10
|
+
* lumira custom logs Show cached outputs from the cache file
|
|
11
|
+
*
|
|
12
|
+
* Design constraints:
|
|
13
|
+
* - No runtime deps beyond Node built-ins.
|
|
14
|
+
* - All FS reads in try/catch — graceful errors, exit 1 on failure.
|
|
15
|
+
* - Color: process.stdout.isTTY ? 'named' : 'none'.
|
|
16
|
+
* - Return type: Promise<{ output: string; exitCode: number }> so the
|
|
17
|
+
* dispatcher can set process.exitCode.
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
20
|
+
import { homedir } from 'node:os';
|
|
21
|
+
import { join, dirname } from 'node:path';
|
|
22
|
+
import { execBg } from '../utils/exec-bg.js';
|
|
23
|
+
import { createColors } from '../render/colors.js';
|
|
24
|
+
import { loadConfig } from '../config.js';
|
|
25
|
+
import { readCacheFile } from '../utils/custom-cache.js';
|
|
26
|
+
// ── constants ──────────────────────────────────────────────────────────────
|
|
27
|
+
const CONFIG_FILE = 'config.json';
|
|
28
|
+
const CONFIG_DIR = join('.config', 'lumira');
|
|
29
|
+
const CACHE_FILE = 'custom-commands.json';
|
|
30
|
+
const CACHE_DIR = join('.cache', 'lumira');
|
|
31
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
32
|
+
function configPath() {
|
|
33
|
+
return join(homedir(), CONFIG_DIR, CONFIG_FILE);
|
|
34
|
+
}
|
|
35
|
+
function cachePath() {
|
|
36
|
+
return join(homedir(), CACHE_DIR, CACHE_FILE);
|
|
37
|
+
}
|
|
38
|
+
function ok(output) {
|
|
39
|
+
return { output, exitCode: 0 };
|
|
40
|
+
}
|
|
41
|
+
function fail(output) {
|
|
42
|
+
return { output, exitCode: 1 };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read the raw config JSON from disk. Returns an empty object `{}` if the
|
|
46
|
+
* file doesn't exist, throws on malformed JSON so the caller can surface a
|
|
47
|
+
* useful error.
|
|
48
|
+
*/
|
|
49
|
+
function readConfigRaw() {
|
|
50
|
+
const p = configPath();
|
|
51
|
+
if (!existsSync(p))
|
|
52
|
+
return {};
|
|
53
|
+
const raw = readFileSync(p, 'utf8');
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
56
|
+
return {};
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Write (or create) the config file. Creates the parent directory if needed.
|
|
61
|
+
* The value is always pretty-printed with 2-space indents.
|
|
62
|
+
*/
|
|
63
|
+
function writeConfigRaw(value) {
|
|
64
|
+
const p = configPath();
|
|
65
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
66
|
+
writeFileSync(p, JSON.stringify(value, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Extract the `customCommands` block from the raw config. Returns a minimal
|
|
70
|
+
* default when the block is missing or malformed.
|
|
71
|
+
*/
|
|
72
|
+
function readCustomCommandsBlock(raw) {
|
|
73
|
+
const cc = raw.customCommands;
|
|
74
|
+
if (!cc || typeof cc !== 'object' || Array.isArray(cc)) {
|
|
75
|
+
return { enabled: false, commands: [] };
|
|
76
|
+
}
|
|
77
|
+
const obj = cc;
|
|
78
|
+
const enabled = typeof obj.enabled === 'boolean' ? obj.enabled : false;
|
|
79
|
+
const commands = Array.isArray(obj.commands) ? obj.commands : [];
|
|
80
|
+
return { enabled, commands };
|
|
81
|
+
}
|
|
82
|
+
// ── color ──────────────────────────────────────────────────────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Only use color when stdout is a real TTY and NO_COLOR is not set.
|
|
85
|
+
* In pipe/test contexts or when NO_COLOR is in env, produces no escape
|
|
86
|
+
* sequences, keeping output clean for programmatic use.
|
|
87
|
+
*/
|
|
88
|
+
function makeColors() {
|
|
89
|
+
const noColor = 'NO_COLOR' in process.env || process.env.TERM === 'dumb';
|
|
90
|
+
if (noColor || !process.stdout.isTTY)
|
|
91
|
+
return null;
|
|
92
|
+
return createColors('named');
|
|
93
|
+
}
|
|
94
|
+
// ── subcommands ────────────────────────────────────────────────────────────
|
|
95
|
+
async function cmdEnable() {
|
|
96
|
+
try {
|
|
97
|
+
const raw = readConfigRaw();
|
|
98
|
+
const cc = readCustomCommandsBlock(raw);
|
|
99
|
+
const updated = {
|
|
100
|
+
...raw,
|
|
101
|
+
customCommands: {
|
|
102
|
+
...cc,
|
|
103
|
+
enabled: true,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
writeConfigRaw(updated);
|
|
107
|
+
return ok(`Custom commands enabled.\nConfig written to: ${configPath()}\n`);
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
111
|
+
return fail(`lumira custom enable: ${msg}\n`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function cmdDisable() {
|
|
115
|
+
try {
|
|
116
|
+
const raw = readConfigRaw();
|
|
117
|
+
const cc = readCustomCommandsBlock(raw);
|
|
118
|
+
const updated = {
|
|
119
|
+
...raw,
|
|
120
|
+
customCommands: {
|
|
121
|
+
...cc,
|
|
122
|
+
enabled: false,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
writeConfigRaw(updated);
|
|
126
|
+
return ok(`Custom commands disabled.\nConfig written to: ${configPath()}\n`);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
130
|
+
return fail(`lumira custom disable: ${msg}\n`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function cmdList() {
|
|
134
|
+
const c = makeColors();
|
|
135
|
+
let enabled = false;
|
|
136
|
+
let commands = [];
|
|
137
|
+
try {
|
|
138
|
+
const cfg = loadConfig();
|
|
139
|
+
enabled = cfg.customCommands.enabled;
|
|
140
|
+
commands = cfg.customCommands.commands;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// config unreadable — fall through with empty defaults
|
|
144
|
+
}
|
|
145
|
+
const statusLine = enabled
|
|
146
|
+
? `Custom commands: ${c ? c.green('enabled') : 'enabled'}\n`
|
|
147
|
+
: `Custom commands: ${c ? c.yellow('disabled') : 'disabled'}\n`;
|
|
148
|
+
if (commands.length === 0) {
|
|
149
|
+
return ok(statusLine
|
|
150
|
+
+ '\nNo custom commands configured.\n'
|
|
151
|
+
+ `Add commands to ${configPath()} under customCommands.commands.\n`);
|
|
152
|
+
}
|
|
153
|
+
// Table: id | line | refresh | cmd
|
|
154
|
+
const header = `${'id'.padEnd(20)} ${'line'.padEnd(6)} ${'refresh'.padEnd(10)} cmd`;
|
|
155
|
+
const sep = '-'.repeat(header.length);
|
|
156
|
+
const rows = commands.map(cmd => {
|
|
157
|
+
const id = cmd.id.padEnd(20);
|
|
158
|
+
const line = String(cmd.line).padEnd(6);
|
|
159
|
+
const refresh = `${cmd.refreshMs}ms`.padEnd(10);
|
|
160
|
+
const cmdStr = cmd.command.join(' ');
|
|
161
|
+
return `${id} ${line} ${refresh} ${cmdStr}`;
|
|
162
|
+
});
|
|
163
|
+
const table = [header, sep, ...rows].join('\n');
|
|
164
|
+
return ok(`${statusLine}\n${table}\n`);
|
|
165
|
+
}
|
|
166
|
+
async function cmdTest(id) {
|
|
167
|
+
if (!id) {
|
|
168
|
+
return fail('lumira custom test: missing command id.\n\n'
|
|
169
|
+
+ 'Usage: lumira custom test <id>\n'
|
|
170
|
+
+ "Use 'lumira custom list' to see configured command ids.\n");
|
|
171
|
+
}
|
|
172
|
+
let commands;
|
|
173
|
+
try {
|
|
174
|
+
commands = loadConfig().customCommands.commands;
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
178
|
+
return fail(`lumira custom test: could not read config: ${msg}\n`);
|
|
179
|
+
}
|
|
180
|
+
const cmd = commands.find(c => c.id === id);
|
|
181
|
+
if (!cmd) {
|
|
182
|
+
const knownIds = commands.map(c => c.id).join(', ');
|
|
183
|
+
return fail(`lumira custom test: command id "${id}" not found.\n`
|
|
184
|
+
+ (knownIds ? `Known ids: ${knownIds}\n` : 'No commands configured.\n'));
|
|
185
|
+
}
|
|
186
|
+
const result = await execBg({
|
|
187
|
+
command: cmd.command,
|
|
188
|
+
timeoutMs: cmd.timeoutMs,
|
|
189
|
+
maxBytes: cmd.maxBytes,
|
|
190
|
+
});
|
|
191
|
+
const lines = [
|
|
192
|
+
`Command: ${cmd.command.join(' ')}`,
|
|
193
|
+
`Duration: ${result.durationMs}ms`,
|
|
194
|
+
];
|
|
195
|
+
if (result.kind === 'ok') {
|
|
196
|
+
lines.push(`Exit: 0 (ok)`);
|
|
197
|
+
lines.push(`Output:\n${result.stdout || '(empty)'}`);
|
|
198
|
+
}
|
|
199
|
+
else if (result.kind === 'nonzero') {
|
|
200
|
+
lines.push(`Exit: ${result.exitCode} (nonzero)`);
|
|
201
|
+
if (result.stdout)
|
|
202
|
+
lines.push(`Stdout:\n${result.stdout}`);
|
|
203
|
+
if (result.stderr)
|
|
204
|
+
lines.push(`Stderr:\n${result.stderr}`);
|
|
205
|
+
}
|
|
206
|
+
else if (result.kind === 'timeout') {
|
|
207
|
+
lines.push(`Exit: timeout (killed after ${cmd.timeoutMs}ms)`);
|
|
208
|
+
if (result.stdout)
|
|
209
|
+
lines.push(`Stdout (partial):\n${result.stdout}`);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// spawn-error
|
|
213
|
+
lines.push(`Exit: spawn-error — ${result.message}`);
|
|
214
|
+
}
|
|
215
|
+
return ok(lines.join('\n') + '\n');
|
|
216
|
+
}
|
|
217
|
+
async function cmdLogs() {
|
|
218
|
+
const p = cachePath();
|
|
219
|
+
const cacheData = readCacheFile(p);
|
|
220
|
+
const entries = Object.entries(cacheData);
|
|
221
|
+
if (entries.length === 0) {
|
|
222
|
+
return ok(`No cache file found at ${p}.\n`
|
|
223
|
+
+ "Run lumira once with custom commands enabled to populate the cache.\n");
|
|
224
|
+
}
|
|
225
|
+
const lines = [`Cache: ${p}`, ''];
|
|
226
|
+
for (const [id, entry] of entries) {
|
|
227
|
+
const { text, capturedAt, state } = entry;
|
|
228
|
+
const dateStr = capturedAt > 0
|
|
229
|
+
? new Date(capturedAt).toLocaleString()
|
|
230
|
+
: 'unknown';
|
|
231
|
+
const truncated = text.length > 100 ? text.slice(0, 100) + '…' : text;
|
|
232
|
+
lines.push(`id: ${id}`);
|
|
233
|
+
lines.push(` state: ${state}`);
|
|
234
|
+
lines.push(` capturedAt: ${dateStr}`);
|
|
235
|
+
lines.push(` text: ${truncated || '(empty)'}`);
|
|
236
|
+
lines.push('');
|
|
237
|
+
}
|
|
238
|
+
return ok(lines.join('\n'));
|
|
239
|
+
}
|
|
240
|
+
function helpText() {
|
|
241
|
+
return [
|
|
242
|
+
'Usage: lumira custom <subcommand>',
|
|
243
|
+
'',
|
|
244
|
+
'Subcommands:',
|
|
245
|
+
' list List configured custom commands',
|
|
246
|
+
' enable Enable custom commands in config',
|
|
247
|
+
' disable Disable custom commands in config',
|
|
248
|
+
' test <id> Run a command once and print output + timing',
|
|
249
|
+
' logs Show cached command outputs',
|
|
250
|
+
'',
|
|
251
|
+
].join('\n');
|
|
252
|
+
}
|
|
253
|
+
// ── entry point ────────────────────────────────────────────────────────────
|
|
254
|
+
/**
|
|
255
|
+
* Execute `lumira custom [subcommand] [...args]`.
|
|
256
|
+
*
|
|
257
|
+
* argv is the full process.argv; 'custom' starts at argv[2], the subcommand
|
|
258
|
+
* at argv[3], additional arguments from argv[4] onward.
|
|
259
|
+
*
|
|
260
|
+
* Returns `{ output, exitCode }` — the dispatcher writes output to stdout and
|
|
261
|
+
* sets process.exitCode from the returned value.
|
|
262
|
+
*/
|
|
263
|
+
export async function runCustomCommand(argv) {
|
|
264
|
+
const sub = argv[3];
|
|
265
|
+
switch (sub) {
|
|
266
|
+
case 'enable':
|
|
267
|
+
return cmdEnable();
|
|
268
|
+
case 'disable':
|
|
269
|
+
return cmdDisable();
|
|
270
|
+
case 'list':
|
|
271
|
+
return cmdList();
|
|
272
|
+
case 'test':
|
|
273
|
+
return cmdTest(argv[4]);
|
|
274
|
+
case 'logs':
|
|
275
|
+
return cmdLogs();
|
|
276
|
+
default:
|
|
277
|
+
return fail(helpText());
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
//# sourceMappingURL=custom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom.js","sourceRoot":"","sources":["../../src/commands/custom.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type SessionStats } from '../parsers/transcript-stats.js';
|
|
2
|
+
export interface StatsArgs {
|
|
3
|
+
sessionId?: string;
|
|
4
|
+
noColor: boolean;
|
|
5
|
+
json: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Result of invoking the stats subcommand. Mirrors `ThemesCommandResult` so
|
|
9
|
+
* the dispatcher in `index.ts` can treat both subcommands uniformly.
|
|
10
|
+
*/
|
|
11
|
+
export interface StatsCommandResult {
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
exitCode: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Optional overrides for `runStatsCommand`. `cwd` and `homeDir` are injected
|
|
18
|
+
* here (rather than read from `process`) so tests can build isolated fake
|
|
19
|
+
* `~/.claude/projects/` trees in tmpdir without touching the real one.
|
|
20
|
+
*/
|
|
21
|
+
export interface StatsCommandOpts {
|
|
22
|
+
cwd?: string;
|
|
23
|
+
homeDir?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse argv for `lumira stats [...flags]`. argv is the full process.argv;
|
|
27
|
+
* the 'stats' command starts at argv[2], flags from argv[3].
|
|
28
|
+
*
|
|
29
|
+
* Per https://no-color.org, the NO_COLOR env var (any non-empty value)
|
|
30
|
+
* disables color. An empty string is *not* a trigger — the variable being
|
|
31
|
+
* unset and the variable being empty must behave identically, otherwise
|
|
32
|
+
* shell environments that pre-export NO_COLOR='' would silently disable
|
|
33
|
+
* color everywhere.
|
|
34
|
+
*
|
|
35
|
+
* Unknown flags are ignored to stay forward-compatible: a future minor that
|
|
36
|
+
* adds a new flag shouldn't crash older binaries piped against new shells.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseStatsArgs(argv: string[]): StatsArgs;
|
|
39
|
+
/**
|
|
40
|
+
* Render a SessionStats either as human-readable text or as pretty JSON.
|
|
41
|
+
*
|
|
42
|
+
* In `json` mode we emit `JSON.stringify(stats, null, 2)` — the 2-space
|
|
43
|
+
* indent makes the output diff-friendly without being absurdly verbose, and
|
|
44
|
+
* matches the convention used elsewhere in the tree for CLI JSON output.
|
|
45
|
+
*
|
|
46
|
+
* In human mode we always render the `Session:` and `Tools:` lines so the
|
|
47
|
+
* user gets a consistent skeleton regardless of which renderer produced the
|
|
48
|
+
* transcript. Cost and burn-rate lines are gated on `hasCostData` so Qwen
|
|
49
|
+
* (and other no-usage backends) never surface a misleading "$0.00".
|
|
50
|
+
*/
|
|
51
|
+
export declare function formatStatsOutput(stats: SessionStats, opts: {
|
|
52
|
+
noColor: boolean;
|
|
53
|
+
json: boolean;
|
|
54
|
+
}): string;
|
|
55
|
+
/**
|
|
56
|
+
* Execute `lumira stats [...]`. Resolution order for the transcript:
|
|
57
|
+
* 1. `--session-id <path>` (contains `/` or ends in `.jsonl`) — used as-is.
|
|
58
|
+
* 2. `--session-id <uuid>` — looked up under cwd-slug, then globally.
|
|
59
|
+
* 3. No flag — auto-discover newest in cwd-slug dir, then globally.
|
|
60
|
+
*
|
|
61
|
+
* The parser's own allow-list check (LUMIRA_ALLOWED_ROOTS) remains the
|
|
62
|
+
* security boundary — anything we hand it must pass `isUnderAllowedRoot`.
|
|
63
|
+
* Discovered paths live under `~/.claude/projects/` (covered by the home
|
|
64
|
+
* root) or under tmpdir in tests (also covered).
|
|
65
|
+
*
|
|
66
|
+
* `cols` is accepted for parity with `runThemesCommand` but the human
|
|
67
|
+
* renderer here doesn't wrap or align to terminal width — keep it in the
|
|
68
|
+
* signature so a future widened renderer (sparklines, multi-column tool
|
|
69
|
+
* frequency) can opt in without breaking the dispatcher contract.
|
|
70
|
+
*/
|
|
71
|
+
export declare function runStatsCommand(argv: string[], _cols?: number, opts?: StatsCommandOpts): Promise<StatsCommandResult>;
|