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.
Files changed (55) hide show
  1. package/README.md +88 -1
  2. package/dist/commands/custom-refresh.d.ts +38 -0
  3. package/dist/commands/custom-refresh.js +124 -0
  4. package/dist/commands/custom-refresh.js.map +1 -0
  5. package/dist/commands/custom.d.ts +15 -0
  6. package/dist/commands/custom.js +280 -0
  7. package/dist/commands/custom.js.map +1 -0
  8. package/dist/commands/stats.d.ts +71 -0
  9. package/dist/commands/stats.js +371 -0
  10. package/dist/commands/stats.js.map +1 -0
  11. package/dist/config.js +150 -6
  12. package/dist/config.js.map +1 -1
  13. package/dist/index.js +66 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/installer.js +1 -0
  16. package/dist/installer.js.map +1 -1
  17. package/dist/parsers/custom-commands.d.ts +79 -0
  18. package/dist/parsers/custom-commands.js +232 -0
  19. package/dist/parsers/custom-commands.js.map +1 -0
  20. package/dist/parsers/transcript-stats.d.ts +41 -0
  21. package/dist/parsers/transcript-stats.js +141 -0
  22. package/dist/parsers/transcript-stats.js.map +1 -0
  23. package/dist/render/index.js +6 -1
  24. package/dist/render/index.js.map +1 -1
  25. package/dist/render/line1.js +9 -1
  26. package/dist/render/line1.js.map +1 -1
  27. package/dist/render/line2.js +9 -1
  28. package/dist/render/line2.js.map +1 -1
  29. package/dist/render/line3.js +9 -1
  30. package/dist/render/line3.js.map +1 -1
  31. package/dist/render/line4.js +20 -9
  32. package/dist/render/line4.js.map +1 -1
  33. package/dist/render/minimal.js +8 -2
  34. package/dist/render/minimal.js.map +1 -1
  35. package/dist/render/powerline-line1.d.ts +1 -1
  36. package/dist/render/powerline-line1.js +19 -3
  37. package/dist/render/powerline-line1.js.map +1 -1
  38. package/dist/render/powerline-line2.js +10 -1
  39. package/dist/render/powerline-line2.js.map +1 -1
  40. package/dist/render/powerline-line3.d.ts +1 -1
  41. package/dist/render/powerline-line3.js +15 -3
  42. package/dist/render/powerline-line3.js.map +1 -1
  43. package/dist/render/shared.d.ts +32 -0
  44. package/dist/render/shared.js +62 -1
  45. package/dist/render/shared.js.map +1 -1
  46. package/dist/types.d.ts +89 -0
  47. package/dist/types.js +19 -0
  48. package/dist/types.js.map +1 -1
  49. package/dist/utils/custom-cache.d.ts +21 -0
  50. package/dist/utils/custom-cache.js +77 -0
  51. package/dist/utils/custom-cache.js.map +1 -0
  52. package/dist/utils/exec-bg.d.ts +50 -0
  53. package/dist/utils/exec-bg.js +183 -0
  54. package/dist/utils/exec-bg.js.map +1 -0
  55. package/package.json +2 -2
package/README.md CHANGED
@@ -24,7 +24,9 @@ Interactive wizard — preset, theme, icons — previewed live before write.
24
24
  ![Claude Code](https://img.shields.io/badge/Claude_Code-compatible-2d3748?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCI+PHBhdGggZD0iTTY0IDEyOEMzNS44IDEyOCAxMyAxMDUuMiAxMyA3N0MxMyA0OC44IDM1LjggMjYgNjQgMjZjMjguMiAwIDUxIDIyLjggNTEgNTFzLTIyLjggNTEtNTEgNTF6IiBmaWxsPSIjMjQyNTJGIi8+PC9zdmc+)
25
25
  ![Qwen Code](https://img.shields.io/badge/Qwen_Code-compatible-6156FF)
26
26
 
27
- > 1,700+ downloads in the first month[share your setup](https://github.com/cativo23/lumira/discussions) in Discussions.
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>;