@gabrihhh/jarvis 2.4.0 → 2.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 +5 -8
- package/bin/jarvis.js +64 -0
- package/package.json +1 -1
- package/src/display.js +1 -23
- package/src/index.js +3 -9
- package/src/reader.js +34 -14
- package/src/statusline.js +12 -13
- package/src/theme.js +30 -0
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Reinicie o Claude Code após o setup.
|
|
|
22
22
|
| Comando | Descrição |
|
|
23
23
|
|---|---|
|
|
24
24
|
| `jarvis` | Mostra a versão |
|
|
25
|
-
| `jarvis --usage` | Dashboard completo de uso (tokens
|
|
25
|
+
| `jarvis --usage` | Dashboard completo de uso (tokens e custo) |
|
|
26
26
|
| `jarvis --watch` | Dashboard com auto-refresh a cada 30s |
|
|
27
27
|
| `jarvis --setup` | Configura status bar, instala slash commands e define trigger padrão |
|
|
28
28
|
| `jarvis --graph` | Abre o Neo4j Browser em localhost:7474 |
|
|
@@ -30,6 +30,10 @@ Reinicie o Claude Code após o setup.
|
|
|
30
30
|
| `jarvis --trigger session` | Hook de memória roda uma vez por sessão *(padrão)* |
|
|
31
31
|
| `jarvis --trigger prompt` | Hook de memória roda a cada prompt |
|
|
32
32
|
| `jarvis --trigger off` | Desativa o carregamento automático de memória |
|
|
33
|
+
| `jarvis --theme` | Mostra o tema atual da status bar |
|
|
34
|
+
| `jarvis --theme <name>:<#hex>` | Define a cor de um box (`context`, `trigger`, `memory`) |
|
|
35
|
+
| `jarvis --theme <name>:reset` | Reseta a cor de um box para o padrão |
|
|
36
|
+
| `jarvis --theme reset` | Reseta todas as cores para o padrão |
|
|
33
37
|
| `jarvis --line` | Saída de uma linha usada internamente pela status bar |
|
|
34
38
|
| `jarvis --help` | Lista todos os comandos |
|
|
35
39
|
|
|
@@ -62,13 +66,6 @@ Reinicie o Claude Code após o setup.
|
|
|
62
66
|
│ │
|
|
63
67
|
├──────────────────────────────────────────────────────────────┤
|
|
64
68
|
│ │
|
|
65
|
-
│ ⬡ Context Window (current session) │
|
|
66
|
-
│ │
|
|
67
|
-
│ ████████████░░░░░░░░░░░░ 52% 103.6K / 200.0K │
|
|
68
|
-
│ 146 turns · model: sonnet-4-6 │
|
|
69
|
-
│ │
|
|
70
|
-
├──────────────────────────────────────────────────────────────┤
|
|
71
|
-
│ │
|
|
72
69
|
│ Monthly breakdown │
|
|
73
70
|
│ Input: 62.5K Output: 1.53M │
|
|
74
71
|
│ Cache read: 232.48M Cache write: 11.25M │
|
package/bin/jarvis.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { run } from '../src/index.js';
|
|
3
3
|
import { renderLine } from '../src/statusline.js';
|
|
4
|
+
import { readTheme, writeTheme, isValidHex, DEFAULT_COLORS, VALID_NAMES, THEME_PATH } from '../src/theme.js';
|
|
4
5
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'fs';
|
|
5
6
|
import { join, dirname } from 'path';
|
|
6
7
|
import { homedir } from 'os';
|
|
@@ -76,6 +77,10 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
76
77
|
jarvis --trigger session Hook runs once per session (default)
|
|
77
78
|
jarvis --trigger prompt Hook runs on every prompt
|
|
78
79
|
jarvis --trigger off Disable automatic memory loading
|
|
80
|
+
jarvis --theme Show current statusline theme
|
|
81
|
+
jarvis --theme <name>:<hex> Set a box color (context, trigger, memory)
|
|
82
|
+
jarvis --theme <name>:reset Reset a single box to default color
|
|
83
|
+
jarvis --theme reset Reset all colors to default
|
|
79
84
|
jarvis --help Show this help
|
|
80
85
|
|
|
81
86
|
Slash commands (inside Claude Code):
|
|
@@ -189,6 +194,65 @@ if (args.includes('--graph')) {
|
|
|
189
194
|
if (mode !== 'off') console.log(` Hook configured in ~/.claude/settings.json`);
|
|
190
195
|
else console.log(` Hook removed from ~/.claude/settings.json`);
|
|
191
196
|
console.log(`\n Restart Claude Code to activate.\n`);
|
|
197
|
+
} else if (args.includes('--theme')) {
|
|
198
|
+
const { Chalk } = await import('chalk');
|
|
199
|
+
const chalk = new Chalk({ level: 3 });
|
|
200
|
+
const value = args[args.indexOf('--theme') + 1];
|
|
201
|
+
|
|
202
|
+
// jarvis --theme → mostra tema atual
|
|
203
|
+
if (!value || value.startsWith('--')) {
|
|
204
|
+
const theme = readTheme();
|
|
205
|
+
console.log('\n Current theme:\n');
|
|
206
|
+
for (const name of VALID_NAMES) {
|
|
207
|
+
const hex = theme[name];
|
|
208
|
+
const isDefault = hex === DEFAULT_COLORS[name];
|
|
209
|
+
const tag = isDefault ? chalk.dim(' (default)') : '';
|
|
210
|
+
console.log(` ${chalk.bold(name.padEnd(8))} ${chalk.hex(hex).bold('██')} ${hex}${tag}`);
|
|
211
|
+
}
|
|
212
|
+
console.log(`\n Config: ${THEME_PATH}\n`);
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// jarvis --theme reset → reseta tudo
|
|
217
|
+
if (value === 'reset') {
|
|
218
|
+
writeTheme({ ...DEFAULT_COLORS });
|
|
219
|
+
console.log('\n ✓ Theme reset to defaults\n');
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// jarvis --theme name:value
|
|
224
|
+
const sep = value.indexOf(':');
|
|
225
|
+
if (sep === -1) {
|
|
226
|
+
console.error(`\n ✗ Invalid format. Use: jarvis --theme <context|trigger|memory>:<#hexcolor|reset>\n`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const name = value.slice(0, sep);
|
|
231
|
+
const color = value.slice(sep + 1);
|
|
232
|
+
|
|
233
|
+
if (!VALID_NAMES.includes(name)) {
|
|
234
|
+
console.error(`\n ✗ Unknown name "${name}". Valid names: ${VALID_NAMES.join(', ')}\n`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const theme = readTheme();
|
|
239
|
+
|
|
240
|
+
if (color === 'reset') {
|
|
241
|
+
theme[name] = DEFAULT_COLORS[name];
|
|
242
|
+
writeTheme(theme);
|
|
243
|
+
console.log(`\n ✓ ${name} reset to default (${DEFAULT_COLORS[name]})\n`);
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!isValidHex(color)) {
|
|
248
|
+
console.error(`\n ✗ Invalid hex color "${color}". Use format: #rgb or #rrggbb\n`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
theme[name] = color;
|
|
253
|
+
writeTheme(theme);
|
|
254
|
+
console.log(`\n ✓ ${name} set to ${chalk.hex(color).bold(color)}\n`);
|
|
255
|
+
process.exit(0);
|
|
192
256
|
} else if (args.includes('--query')) {
|
|
193
257
|
try {
|
|
194
258
|
const { queryByPath } = await import('../src/memory/query-by-path.js');
|
package/package.json
CHANGED
package/src/display.js
CHANGED
|
@@ -60,29 +60,13 @@ function tokenRow(label, stats, maxTokens) {
|
|
|
60
60
|
return row(text);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
// ─── Context window row ──────────────────────────────────────
|
|
64
|
-
function contextRow(session) {
|
|
65
|
-
if (!session) {
|
|
66
|
-
return row(` ${chalk.hex(DIM)('No active session detected')}`);
|
|
67
|
-
}
|
|
68
|
-
const { contextUsed, contextWindow, percent, model, turns } = session;
|
|
69
|
-
const b = bar(percent, 24);
|
|
70
|
-
const pct = chalk.bold.hex(WHITE)(`${percent}%`);
|
|
71
|
-
const info = chalk.hex(WHITE)(`${formatTokens(contextUsed)} / ${formatTokens(contextWindow)}`);
|
|
72
|
-
const meta = chalk.hex(DIM)(`${turns} turn${turns !== 1 ? 's' : ''} · model: ${model.replace('claude-', '')}`);
|
|
73
|
-
return [
|
|
74
|
-
row(` ${b} ${pct} ${info}`),
|
|
75
|
-
row(` ${meta}`),
|
|
76
|
-
].join('\n');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
63
|
// ─── Section header ──────────────────────────────────────────
|
|
80
64
|
function sectionHeader(icon, label) {
|
|
81
65
|
return row(` ${chalk.hex(WHITE)(icon)} ${chalk.bold.hex(WHITE)(label)}`);
|
|
82
66
|
}
|
|
83
67
|
|
|
84
68
|
// ─── Full render ─────────────────────────────────────────────
|
|
85
|
-
export function render(stats
|
|
69
|
+
export function render(stats) {
|
|
86
70
|
const { monthly, weekly, daily } = stats;
|
|
87
71
|
const maxTokens = monthly.total || 1;
|
|
88
72
|
|
|
@@ -108,12 +92,6 @@ export function render(stats, session) {
|
|
|
108
92
|
row(''),
|
|
109
93
|
DIV,
|
|
110
94
|
row(''),
|
|
111
|
-
sectionHeader('⬡', 'Context Window (current session)'),
|
|
112
|
-
row(''),
|
|
113
|
-
contextRow(session),
|
|
114
|
-
row(''),
|
|
115
|
-
DIV,
|
|
116
|
-
row(''),
|
|
117
95
|
breakdown(monthly),
|
|
118
96
|
row(''),
|
|
119
97
|
BOT,
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readAllUsage
|
|
2
|
-
import { aggregateStats
|
|
1
|
+
import { readAllUsage } from './reader.js';
|
|
2
|
+
import { aggregateStats } from './calculator.js';
|
|
3
3
|
import { render, renderError, renderLoading } from './display.js';
|
|
4
4
|
|
|
5
5
|
export async function run() {
|
|
@@ -15,12 +15,6 @@ export async function run() {
|
|
|
15
15
|
|
|
16
16
|
const stats = aggregateStats(allEntries);
|
|
17
17
|
|
|
18
|
-
// current session
|
|
19
|
-
const sessionMeta = getCurrentSessionFile();
|
|
20
|
-
const sessionId = sessionMeta?.sessionId;
|
|
21
|
-
const sessionEntries = sessionId ? readCurrentSessionUsage(sessionId) : [];
|
|
22
|
-
const session = aggregateSession(sessionEntries);
|
|
23
|
-
|
|
24
18
|
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
25
|
-
console.log('\n' + render(stats
|
|
19
|
+
console.log('\n' + render(stats) + '\n');
|
|
26
20
|
}
|
package/src/reader.js
CHANGED
|
@@ -3,6 +3,34 @@ import { join } from 'path';
|
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
|
|
6
|
+
function getParentPid(pid) {
|
|
7
|
+
// Linux — leitura direta do /proc, sem subprocess
|
|
8
|
+
try {
|
|
9
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
10
|
+
const match = stat.match(/^\d+ \(.*?\) \S+ (\d+)/);
|
|
11
|
+
if (match) return parseInt(match[1]);
|
|
12
|
+
} catch { /* não é Linux ou /proc indisponível */ }
|
|
13
|
+
|
|
14
|
+
// macOS / Linux fallback
|
|
15
|
+
if (process.platform !== 'win32') {
|
|
16
|
+
try {
|
|
17
|
+
const out = execSync(`ps -o ppid= -p ${pid}`, { stdio: 'pipe' }).toString().trim();
|
|
18
|
+
const n = parseInt(out);
|
|
19
|
+
return isNaN(n) ? null : n;
|
|
20
|
+
} catch { return null; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Windows — PowerShell com Get-CimInstance (não deprecated, Windows 7+)
|
|
24
|
+
try {
|
|
25
|
+
const out = execSync(
|
|
26
|
+
`powershell -NoProfile -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').ParentProcessId"`,
|
|
27
|
+
{ stdio: 'pipe' }
|
|
28
|
+
).toString().trim();
|
|
29
|
+
const n = parseInt(out);
|
|
30
|
+
return isNaN(n) ? null : n;
|
|
31
|
+
} catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
6
34
|
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
7
35
|
const PROJECTS_DIR = join(CLAUDE_DIR, 'projects');
|
|
8
36
|
|
|
@@ -95,24 +123,16 @@ export function getCurrentSessionFile() {
|
|
|
95
123
|
if (!existsSync(sessionsDir)) return null;
|
|
96
124
|
|
|
97
125
|
try {
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// also check grandparent in case the command runs through a shell
|
|
103
|
-
try {
|
|
104
|
-
const grandparent = parseInt(
|
|
105
|
-
execSync(`ps -o ppid= -p ${process.ppid}`, { stdio: ['pipe','pipe','pipe'] }).toString().trim()
|
|
106
|
-
);
|
|
107
|
-
if (grandparent) pidsToCheck.add(grandparent);
|
|
108
|
-
} catch { /* skip */ }
|
|
109
|
-
|
|
110
|
-
for (const pid of pidsToCheck) {
|
|
126
|
+
// Sobe a árvore de processos até 5 níveis para encontrar o .json da sessão Claude
|
|
127
|
+
let pid = process.ppid;
|
|
128
|
+
for (let i = 0; i < 5; i++) {
|
|
129
|
+
if (!pid || pid <= 1) break;
|
|
111
130
|
const candidate = join(sessionsDir, `${pid}.json`);
|
|
112
131
|
if (existsSync(candidate)) {
|
|
113
132
|
try { return JSON.parse(readFileSync(candidate, 'utf-8')); }
|
|
114
|
-
catch {
|
|
133
|
+
catch { break; }
|
|
115
134
|
}
|
|
135
|
+
pid = getParentPid(pid);
|
|
116
136
|
}
|
|
117
137
|
|
|
118
138
|
// Fallback: find a session whose sessionId appears in JSONL files
|
package/src/statusline.js
CHANGED
|
@@ -4,13 +4,10 @@ import { join } from 'path';
|
|
|
4
4
|
import { homedir, tmpdir } from 'os';
|
|
5
5
|
import { readAllUsage, getCurrentSessionFile, readCurrentSessionUsage } from './reader.js';
|
|
6
6
|
import { aggregateStats, aggregateSession } from './calculator.js';
|
|
7
|
+
import { readTheme } from './theme.js';
|
|
7
8
|
|
|
8
9
|
const chalk = new Chalk({ level: 3 });
|
|
9
10
|
|
|
10
|
-
const PINK = '#f472b6';
|
|
11
|
-
const CYAN = '#22d3ee';
|
|
12
|
-
const GREEN = '#4ade80';
|
|
13
|
-
|
|
14
11
|
function bar(percent, width = 8) {
|
|
15
12
|
const filled = Math.round((percent / 100) * width);
|
|
16
13
|
const empty = width - filled;
|
|
@@ -57,22 +54,24 @@ function isMemoryLoaded(sessionId) {
|
|
|
57
54
|
|
|
58
55
|
export function renderLine() {
|
|
59
56
|
const mode = readTriggerMode();
|
|
60
|
-
const
|
|
61
|
-
const triggerBox = buildBox(triggerInner, PINK);
|
|
57
|
+
const theme = readTheme();
|
|
62
58
|
|
|
63
59
|
const sessionMeta = getCurrentSessionFile();
|
|
64
60
|
const sessionId = sessionMeta?.sessionId;
|
|
65
61
|
const loaded = isMemoryLoaded(sessionId);
|
|
66
|
-
const loadedBox = loaded ? buildBox(' ⬡ ',
|
|
62
|
+
const loadedBox = loaded ? buildBox(' ⬡ ', theme.memory, 4) : null;
|
|
67
63
|
|
|
68
64
|
const allEntries = readAllUsage();
|
|
69
65
|
|
|
70
|
-
const boxes = (contextBox) =>
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
const boxes = (contextBox) => {
|
|
67
|
+
const toJoin = [contextBox];
|
|
68
|
+
if (mode !== 'off') toJoin.push(buildBox(` TRIGGER ${mode.toUpperCase()} `, theme.trigger));
|
|
69
|
+
if (loadedBox) toJoin.push(loadedBox);
|
|
70
|
+
return joinBoxes(...toJoin);
|
|
71
|
+
};
|
|
73
72
|
|
|
74
73
|
if (!allEntries.length) {
|
|
75
|
-
const contextBox = buildBox(` CONTEXT ${'░'.repeat(8)} 0% `,
|
|
74
|
+
const contextBox = buildBox(` CONTEXT ${'░'.repeat(8)} 0% `, theme.context);
|
|
76
75
|
process.stdout.write(boxes(contextBox));
|
|
77
76
|
return;
|
|
78
77
|
}
|
|
@@ -81,11 +80,11 @@ export function renderLine() {
|
|
|
81
80
|
const session = aggregateSession(sessionEntries);
|
|
82
81
|
|
|
83
82
|
if (!session) {
|
|
84
|
-
const contextBox = buildBox(` CONTEXT ${'░'.repeat(8)} 0% `,
|
|
83
|
+
const contextBox = buildBox(` CONTEXT ${'░'.repeat(8)} 0% `, theme.context);
|
|
85
84
|
process.stdout.write(boxes(contextBox));
|
|
86
85
|
return;
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
const contextBox = buildBox(` CONTEXT ${bar(session.percent)} ${session.percent}% `,
|
|
88
|
+
const contextBox = buildBox(` CONTEXT ${bar(session.percent)} ${session.percent}% `, theme.context);
|
|
90
89
|
process.stdout.write(boxes(contextBox));
|
|
91
90
|
}
|
package/src/theme.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
export const THEME_PATH = join(homedir(), '.claude', 'jarvis-theme.json');
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_COLORS = {
|
|
8
|
+
context: '#22d3ee',
|
|
9
|
+
trigger: '#f472b6',
|
|
10
|
+
memory: '#4ade80',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const VALID_NAMES = Object.keys(DEFAULT_COLORS);
|
|
14
|
+
|
|
15
|
+
export function readTheme() {
|
|
16
|
+
try {
|
|
17
|
+
const raw = JSON.parse(readFileSync(THEME_PATH, 'utf8'));
|
|
18
|
+
return { ...DEFAULT_COLORS, ...raw };
|
|
19
|
+
} catch {
|
|
20
|
+
return { ...DEFAULT_COLORS };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function writeTheme(theme) {
|
|
25
|
+
writeFileSync(THEME_PATH, JSON.stringify(theme, null, 2));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isValidHex(value) {
|
|
29
|
+
return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value);
|
|
30
|
+
}
|