moodline 0.1.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/LICENSE +21 -0
- package/README.md +150 -0
- package/bin/moodline.js +244 -0
- package/lib/moodline-core.mjs +386 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alison Amorim
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# 🌿 moodline
|
|
2
|
+
|
|
3
|
+
> Statusline divertida e informativa para CLIs de IA. Barra de contexto em gradiente, emoji que reage à ocupação, git, custo da sessão e trocadilhos de dev — instalável com um comando.
|
|
4
|
+
|
|
5
|
+
Feita pra quem vive no terminal com agentes de código. Mostra de relance **quanto contexto ainda sobra** (antes de tomar um `/compact` na cara), o **modelo e o effort**, e ainda solta um **trocadilho** pra alegrar o `git push`.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Opus high [█▒▒░░░░░░░░░░░░░░░░░░] 😎 5% 10k · 🌿 main · 💬 commit -m "ajustes"
|
|
9
|
+
Opus high [█████▒▒░░░░░░░░░░░░░░] 🙂 25% 50k · 🌿 main* · 💸 $0.08 ⏱ 4m
|
|
10
|
+
Opus high [██████████▒▒░░░░░░░░░] 😅 50% 100k · 🌿 feat/bar ↑2 · 💸 $0.21 ⏱ 12m +120/-30
|
|
11
|
+
Opus high [███████████████▒▒░░░░] 🥵 75% 150k · ⏳ 5h 42% 7d 13%
|
|
12
|
+
Opus high [██████████████████▒▒░] 💀 92% 184k · 🌿 main · 404: piada nao encontrada
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
A cor da barra é interpolada de forma contínua no espaço HSL, do verde (matiz 120°) ao vermelho (0°). O emoji vai de 😎 (tranquilo) a 💀 (hora de dar `/clear`). Quando o terminal é estreito, os segmentos extras somem da direita pra esquerda — o essencial (modelo, barra, %, tokens) nunca cai.
|
|
16
|
+
|
|
17
|
+
## Instalação
|
|
18
|
+
|
|
19
|
+
Uma linha. Detecta as CLIs instaladas e configura cada uma:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx moodline init
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Depois é só abrir uma sessão do Claude Code (ou do Copilot CLI). Pra testar a barra na hora, sem abrir sessão:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
echo '{"model":{"display_name":"Opus"},"effort":{"level":"high"},"context_window":{"used_percentage":92,"total_input_tokens":184000}}' | npx moodline render
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Compatibilidade com as CLIs
|
|
32
|
+
|
|
33
|
+
A barra precisa que a CLI rode um **comando** e mande os dados via **JSON no stdin**. Nem toda CLI de IA suporta isso. Situação atual:
|
|
34
|
+
|
|
35
|
+
| CLI | Statusline custom? | moodline |
|
|
36
|
+
|-----|:---:|-----|
|
|
37
|
+
| **Claude Code** | ✅ Nativo | **Suportado.** Configurado pelo `init`. |
|
|
38
|
+
| **GitHub Copilot CLI** | ⚗️ Experimental | **Suportado.** O `init` liga a feature flag `STATUS_LINE`. |
|
|
39
|
+
| **Gemini CLI** | ❌ Só footer fixo | Experimental — só via extensão HUD de terceiros (hooks + scroll-region). Não é statusline por comando. |
|
|
40
|
+
| **OpenCode** | ❌ TUI fixa | Experimental — `moodline watch` lê a API HTTP e renderiza num painel tmux/zellij (fora da TUI). |
|
|
41
|
+
| **Junie (JetBrains)** | ❌ Não suporta | Sem suporte. O único hook (`SessionStart`) **descarta o output**, então não dá pra renderizar uma barra. |
|
|
42
|
+
|
|
43
|
+
Resumo: **Claude Code e Copilot CLI funcionam de verdade hoje**, porque compartilham o mesmo modelo (comando + JSON no stdin) com schemas quase idênticos — um único engine serve os dois. Os outros três dependem de mecanismos diferentes; veja [Outras CLIs](#outras-clis).
|
|
44
|
+
|
|
45
|
+
## Como funciona
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
npx moodline init
|
|
49
|
+
└─ copia o engine (lib/moodline-core.mjs) pra ~/.claude/moodline/ e ~/.copilot/moodline/
|
|
50
|
+
└─ escreve um config.json com as features escolhidas
|
|
51
|
+
└─ aponta o settings.json da CLI pra: node ".../moodline-core.mjs" --adapter=claude --config=...
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A cada atualização, a CLI executa o engine passando o JSON da sessão no stdin. O engine **normaliza** os campos (via um *adapter* por CLI), monta a barra e imprime no stdout. Zero dependências de runtime, então o render é rápido.
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
A base sempre aparece: **modelo · effort · barra de contexto em gradiente · emoji-humor · % · tokens em `###k`**. Os extras são ligáveis:
|
|
59
|
+
|
|
60
|
+
| Feature | Flag | O que mostra |
|
|
61
|
+
|---------|------|--------------|
|
|
62
|
+
| **git** | `git` | 🌿 branch + `*` (dirty) `↑n` (ahead) `↓n` (behind) |
|
|
63
|
+
| **cost** | `cost` | 💸 custo USD da sessão · ⏱ tempo · `+linhas/-linhas` |
|
|
64
|
+
| **rate** | `rate` | ⏳ uso das janelas 5h e 7d (Claude Pro/Max; some se ausente) |
|
|
65
|
+
| **puns** | `puns` | 💬 trocadilho de dev rotativo (troca a cada ~30s) |
|
|
66
|
+
|
|
67
|
+
Tudo ligado por padrão. Pra escolher:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx moodline init --features=git,cost # só git e custo
|
|
71
|
+
npx moodline init --no-puns --no-rate # tudo menos trocadilhos e rate limits
|
|
72
|
+
npx moodline init --multi # layout em 2 linhas
|
|
73
|
+
npx moodline init --all # força Claude Code E Copilot CLI
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Comandos
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
moodline init Configura a statusline nas CLIs detectadas
|
|
80
|
+
moodline render Lê JSON no stdin e imprime a barra (teste)
|
|
81
|
+
moodline doctor Mostra o que está instalado e configurado
|
|
82
|
+
moodline uninstall Remove a statusLine das CLIs
|
|
83
|
+
moodline watch [experimental] Poller pro OpenCode → stdout
|
|
84
|
+
moodline --help
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Configuração manual
|
|
88
|
+
|
|
89
|
+
O `init` faz tudo, mas se preferir na mão — `~/.claude/settings.json`:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"statusLine": {
|
|
94
|
+
"type": "command",
|
|
95
|
+
"command": "node \"C:/Users/voce/.claude/moodline/moodline-core.mjs\" --adapter=claude",
|
|
96
|
+
"padding": 0,
|
|
97
|
+
"refreshInterval": 5
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use barra normal `/` no caminho mesmo no Windows (o Git Bash trata `\` como escape). O `--config=...` é opcional; sem ele, todas as features ficam ligadas.
|
|
103
|
+
|
|
104
|
+
O `config.json`:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"layout": "single",
|
|
109
|
+
"bar": { "width": 20 },
|
|
110
|
+
"punRotateMs": 30000,
|
|
111
|
+
"features": { "git": true, "cost": true, "rate": true, "puns": true }
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Outras CLIs
|
|
116
|
+
|
|
117
|
+
- **Gemini CLI** — tem footer embutido com toggles (`ui.footer.*`), mas não roda um comando seu pra renderizar conteúdo arbitrário. Dá pra ter uma barra custom só via extensão de terceiros (estilo `gemini-cli-hud`, que usa hooks + escape de scroll-region). No roadmap do moodline como adapter experimental.
|
|
118
|
+
- **OpenCode** — a barra da TUI é fixa. O caminho é externo: `moodline watch --port 4096` consulta a API HTTP/SSE do OpenCode e imprime a barra, pra você fixar num painel do tmux/zellij. Experimental (o endpoint pode mudar entre versões).
|
|
119
|
+
- **Junie (JetBrains)** — a CLI existe (beta), mas não tem statusline e o hook `SessionStart` descarta o stdout. Sem caminho viável hoje.
|
|
120
|
+
|
|
121
|
+
## Requisitos
|
|
122
|
+
|
|
123
|
+
- **Node.js ≥ 18** (o engine é JS puro, sem dependências).
|
|
124
|
+
- Terminal com **truecolor** pro gradiente 24-bit: Windows Terminal, WezTerm, iTerm2, Kitty ou o terminal do VS Code.
|
|
125
|
+
|
|
126
|
+
## Desenvolvimento
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
git clone https://github.com/slipalison/moodline
|
|
130
|
+
cd moodline
|
|
131
|
+
npm test # smoke tests (sem framework, só node)
|
|
132
|
+
echo '{"model":{"display_name":"Opus"},"context_window":{"used_percentage":50,"total_input_tokens":100000}}' | node bin/moodline.js render
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Arquitetura: `lib/moodline-core.mjs` é o engine auto-contido (render + adapters + git + puns). `bin/moodline.js` é o instalador/CLI. Adicionar uma CLI = escrever um adapter `fromX(json)` que normaliza pro mesmo formato de estado.
|
|
136
|
+
|
|
137
|
+
### Release
|
|
138
|
+
|
|
139
|
+
Versionamento começa no `v0`. Publicação é automática via GitHub Action ao empurrar uma tag:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm version patch # bumpa 0.1.0 -> 0.1.1 e cria a tag v0.1.1
|
|
143
|
+
git push --follow-tags # dispara o workflow release.yml (publica no npm + cria o GitHub Release)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Requer o secret `NPM_TOKEN` no repositório (token de Automation do npm).
|
|
147
|
+
|
|
148
|
+
## Licença
|
|
149
|
+
|
|
150
|
+
MIT © Alison Amorim
|
package/bin/moodline.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// moodline — instalador/configurador da statusline para CLIs de IA.
|
|
3
|
+
//
|
|
4
|
+
// Comandos:
|
|
5
|
+
// moodline init configura a(s) CLI(s) detectada(s) (Claude Code, Copilot CLI)
|
|
6
|
+
// moodline render le JSON no stdin e imprime a barra (pra testar)
|
|
7
|
+
// moodline doctor mostra o que esta instalado/configurado
|
|
8
|
+
// moodline uninstall remove a statusLine das CLIs
|
|
9
|
+
// moodline watch [EXPERIMENTAL] poller pro OpenCode (HTTP) -> stdout
|
|
10
|
+
// moodline --help|--version
|
|
11
|
+
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
16
|
+
import { render, ADAPTERS, fromOpenCode, loadConfig, attachGit, DEFAULT_CFG } from '../lib/moodline-core.mjs';
|
|
17
|
+
|
|
18
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const CORE_SRC = join(HERE, '..', 'lib', 'moodline-core.mjs');
|
|
20
|
+
const PKG = JSON.parse(readFileSync(join(HERE, '..', 'package.json'), 'utf8'));
|
|
21
|
+
|
|
22
|
+
const C = { cyan: '\x1b[36m', green: '\x1b[32m', yellow: '\x1b[33m', dim: '\x1b[2m', red: '\x1b[31m', reset: '\x1b[0m', bold: '\x1b[1m' };
|
|
23
|
+
const ok = (s) => console.log(`${C.green}✓${C.reset} ${s}`);
|
|
24
|
+
const warn = (s) => console.log(`${C.yellow}!${C.reset} ${s}`);
|
|
25
|
+
const info = (s) => console.log(` ${C.dim}${s}${C.reset}`);
|
|
26
|
+
|
|
27
|
+
// --------- arg parsing ---------
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const o = { _: [] };
|
|
30
|
+
for (const a of argv) {
|
|
31
|
+
if (a.startsWith('--')) {
|
|
32
|
+
const [k, v] = a.slice(2).split('=');
|
|
33
|
+
o[k] = v === undefined ? true : v;
|
|
34
|
+
} else o._.push(a);
|
|
35
|
+
}
|
|
36
|
+
return o;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --------- alvos de CLI suportados (statusLine nativa por comando + JSON no stdin) ---------
|
|
40
|
+
const TARGETS = {
|
|
41
|
+
claude: {
|
|
42
|
+
label: 'Claude Code',
|
|
43
|
+
dir: join(homedir(), '.claude'),
|
|
44
|
+
settings: join(homedir(), '.claude', 'settings.json'),
|
|
45
|
+
adapter: 'claude',
|
|
46
|
+
patch(s, command) {
|
|
47
|
+
s.statusLine = { type: 'command', command, padding: 0, refreshInterval: 5 };
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
copilot: {
|
|
51
|
+
label: 'GitHub Copilot CLI',
|
|
52
|
+
dir: join(homedir(), '.copilot'),
|
|
53
|
+
settings: join(homedir(), '.copilot', 'settings.json'),
|
|
54
|
+
adapter: 'copilot',
|
|
55
|
+
patch(s, command) {
|
|
56
|
+
s.statusLine = { type: 'command', command, padding: 1 };
|
|
57
|
+
// statusLine no Copilot e experimental: liga a feature flag
|
|
58
|
+
s.feature_flags = s.feature_flags || {};
|
|
59
|
+
s.feature_flags.enabled = Array.from(new Set([...(s.feature_flags.enabled || []), 'STATUS_LINE']));
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function readJson(path) {
|
|
65
|
+
if (!existsSync(path)) return {};
|
|
66
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return {}; }
|
|
67
|
+
}
|
|
68
|
+
function fwd(p) { return p.replace(/\\/g, '/'); }
|
|
69
|
+
|
|
70
|
+
function buildFeatures(opts) {
|
|
71
|
+
const f = structuredClone(DEFAULT_CFG.features);
|
|
72
|
+
if (typeof opts.features === 'string') {
|
|
73
|
+
const want = new Set(opts.features.split(',').map((x) => x.trim()).filter(Boolean));
|
|
74
|
+
for (const k of Object.keys(f)) f[k] = want.has(k);
|
|
75
|
+
}
|
|
76
|
+
for (const k of Object.keys(f)) {
|
|
77
|
+
if (opts[`no-${k}`]) f[k] = false;
|
|
78
|
+
}
|
|
79
|
+
return f;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function configureTarget(key, opts) {
|
|
83
|
+
const t = TARGETS[key];
|
|
84
|
+
const root = join(t.dir, 'moodline');
|
|
85
|
+
const coreDest = join(root, 'moodline-core.mjs');
|
|
86
|
+
const cfgDest = join(root, 'config.json');
|
|
87
|
+
|
|
88
|
+
mkdirSync(root, { recursive: true });
|
|
89
|
+
copyFileSync(CORE_SRC, coreDest);
|
|
90
|
+
|
|
91
|
+
const cfg = { ...structuredClone(DEFAULT_CFG), features: buildFeatures(opts) };
|
|
92
|
+
if (opts.multi) cfg.layout = 'multi';
|
|
93
|
+
writeFileSync(cfgDest, JSON.stringify(cfg, null, 2) + '\n');
|
|
94
|
+
|
|
95
|
+
const command = `node "${fwd(coreDest)}" --adapter=${t.adapter} --config="${fwd(cfgDest)}"`;
|
|
96
|
+
const settings = readJson(t.settings);
|
|
97
|
+
t.patch(settings, command);
|
|
98
|
+
mkdirSync(dirname(t.settings), { recursive: true });
|
|
99
|
+
writeFileSync(t.settings, JSON.stringify(settings, null, 2) + '\n');
|
|
100
|
+
|
|
101
|
+
ok(`${t.label} configurado`);
|
|
102
|
+
info(`engine: ${fwd(coreDest)}`);
|
|
103
|
+
info(`config: ${fwd(cfgDest)}`);
|
|
104
|
+
info(`settings: ${fwd(t.settings)}`);
|
|
105
|
+
return { coreDest, cfgDest, command };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function cmdInit(opts) {
|
|
109
|
+
console.log(`${C.bold}\u{1F33F} moodline init${C.reset}\n`);
|
|
110
|
+
|
|
111
|
+
const explicit = ['claude', 'copilot'].filter((k) => opts[k] || opts.all);
|
|
112
|
+
let keys;
|
|
113
|
+
if (explicit.length) {
|
|
114
|
+
keys = explicit;
|
|
115
|
+
} else {
|
|
116
|
+
// default: Claude Code sempre; Copilot se detectado
|
|
117
|
+
keys = ['claude'];
|
|
118
|
+
if (existsSync(TARGETS.copilot.dir)) keys.push('copilot');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const k of keys) configureTarget(k, opts);
|
|
122
|
+
|
|
123
|
+
console.log();
|
|
124
|
+
if (keys.includes('copilot')) {
|
|
125
|
+
warn('Copilot CLI: statusLine e experimental — reinicie o copilot ou rode `/experimental` se a barra nao aparecer.');
|
|
126
|
+
}
|
|
127
|
+
console.log(`\n${C.bold}Outras CLIs:${C.reset}`);
|
|
128
|
+
info('Gemini CLI — sem statusLine por comando; so footer fixo (toggles) ou extensao HUD 3rd-party. [experimental]');
|
|
129
|
+
info('OpenCode — sem statusLine na TUI; use `moodline watch` num painel tmux/zellij. [experimental]');
|
|
130
|
+
info('Junie CLI — nao suporta statusline (hook SessionStart descarta o output). Sem suporte.');
|
|
131
|
+
console.log(`\n${C.green}Pronto.${C.reset} Abra uma sessao da CLI pra ver a barra. Teste agora: ${C.cyan}moodline render${C.reset}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function cmdUninstall(opts) {
|
|
135
|
+
const keys = ['claude', 'copilot'].filter((k) => opts[k] || opts.all);
|
|
136
|
+
const list = keys.length ? keys : ['claude', 'copilot'];
|
|
137
|
+
for (const k of list) {
|
|
138
|
+
const t = TARGETS[k];
|
|
139
|
+
if (!existsSync(t.settings)) continue;
|
|
140
|
+
const s = readJson(t.settings);
|
|
141
|
+
if (s.statusLine) { delete s.statusLine; writeFileSync(t.settings, JSON.stringify(s, null, 2) + '\n'); ok(`${t.label}: statusLine removida`); }
|
|
142
|
+
else info(`${t.label}: nada pra remover`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function cmdDoctor() {
|
|
147
|
+
console.log(`${C.bold}moodline doctor${C.reset} v${PKG.version}\n`);
|
|
148
|
+
info(`home: ${fwd(homedir())}`);
|
|
149
|
+
for (const [k, t] of Object.entries(TARGETS)) {
|
|
150
|
+
const has = existsSync(t.dir);
|
|
151
|
+
const s = readJson(t.settings);
|
|
152
|
+
const wired = !!s.statusLine && /moodline-core\.mjs/.test(s.statusLine.command || '');
|
|
153
|
+
console.log(`\n${C.bold}${t.label}${C.reset} (${k})`);
|
|
154
|
+
info(`dir existe: ${has ? 'sim' : 'nao'}`);
|
|
155
|
+
info(`statusLine moodline: ${wired ? C.green + 'ativa' + C.reset : 'nao'}`);
|
|
156
|
+
if (s.statusLine?.command) info(`command: ${s.statusLine.command}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function cmdRender(opts) {
|
|
161
|
+
// delega pro engine: le stdin, imprime barra. Reusa as mesmas flags.
|
|
162
|
+
const adapterName = (opts.adapter || 'claude').toLowerCase();
|
|
163
|
+
const adapter = ADAPTERS[adapterName] || ADAPTERS.claude;
|
|
164
|
+
const cfg = loadConfig(opts.config);
|
|
165
|
+
for (const k of ['git', 'cost', 'rate', 'puns']) if (opts[`no-${k}`]) cfg.features[k] = false;
|
|
166
|
+
if (opts.multi) cfg.layout = 'multi';
|
|
167
|
+
if (opts.width) cfg.width = parseInt(opts.width, 10) || undefined;
|
|
168
|
+
|
|
169
|
+
let raw = '';
|
|
170
|
+
try { raw = readFileSync(0, 'utf8'); } catch {}
|
|
171
|
+
if (!raw.trim()) {
|
|
172
|
+
// sem stdin: demo
|
|
173
|
+
raw = JSON.stringify({ model: { display_name: 'Opus' }, effort: { level: 'high' }, context_window: { used_percentage: 42, total_input_tokens: 84000 }, cost: { total_cost_usd: 0.12, total_duration_ms: 320000, total_lines_added: 120, total_lines_removed: 30 } });
|
|
174
|
+
warn('sem stdin — mostrando demo:');
|
|
175
|
+
}
|
|
176
|
+
let j = {};
|
|
177
|
+
try { j = JSON.parse(raw); } catch {}
|
|
178
|
+
const state = attachGit(adapter(j), cfg.features.git);
|
|
179
|
+
console.log(render(state, cfg));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function cmdWatch(opts) {
|
|
183
|
+
// EXPERIMENTAL: poller pro OpenCode. Endpoint pode variar por versao.
|
|
184
|
+
const port = opts.port || 4096;
|
|
185
|
+
const url = opts.url || `http://127.0.0.1:${port}/session`;
|
|
186
|
+
const cfg = loadConfig(opts.config);
|
|
187
|
+
warn(`[experimental] OpenCode watch: GET ${url} a cada ${opts.interval || 2}s`);
|
|
188
|
+
const interval = (parseInt(opts.interval, 10) || 2) * 1000;
|
|
189
|
+
const tick = async () => {
|
|
190
|
+
try {
|
|
191
|
+
const r = await fetch(url);
|
|
192
|
+
const j = await r.json();
|
|
193
|
+
const state = fromOpenCode(Array.isArray(j) ? j[0] : j);
|
|
194
|
+
process.stdout.write('\r\x1b[K' + render(state, cfg));
|
|
195
|
+
} catch (e) {
|
|
196
|
+
process.stdout.write('\r\x1b[K' + C.dim + 'moodline: aguardando OpenCode em ' + url + C.reset);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
await tick();
|
|
200
|
+
setInterval(tick, interval);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function cmdHelp() {
|
|
204
|
+
console.log(`${C.bold}\u{1F33F} moodline${C.reset} v${PKG.version} — statusline divertida pra CLIs de IA
|
|
205
|
+
|
|
206
|
+
${C.bold}Uso:${C.reset}
|
|
207
|
+
npx moodline <comando> [opcoes]
|
|
208
|
+
|
|
209
|
+
${C.bold}Comandos:${C.reset}
|
|
210
|
+
init Configura a statusline nas CLIs detectadas (Claude Code, Copilot CLI)
|
|
211
|
+
render Le JSON no stdin e imprime a barra (pra testar)
|
|
212
|
+
doctor Mostra o que esta instalado e configurado
|
|
213
|
+
uninstall Remove a statusLine das CLIs
|
|
214
|
+
watch [experimental] Poller pro OpenCode -> stdout (use num painel tmux)
|
|
215
|
+
|
|
216
|
+
${C.bold}Opcoes do init:${C.reset}
|
|
217
|
+
--all Configura Claude Code E Copilot CLI
|
|
218
|
+
--claude / --copilot So a CLI escolhida
|
|
219
|
+
--features=git,cost Liga so essas features (git,cost,rate,puns)
|
|
220
|
+
--no-puns --no-rate Desliga uma feature especifica
|
|
221
|
+
--multi Layout em 2 linhas
|
|
222
|
+
|
|
223
|
+
${C.bold}Exemplos:${C.reset}
|
|
224
|
+
npx moodline init
|
|
225
|
+
npx moodline init --all --no-rate
|
|
226
|
+
echo '{"model":{"display_name":"Opus"},"context_window":{"used_percentage":92,"total_input_tokens":184000}}' | npx moodline render
|
|
227
|
+
`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --------- dispatch ---------
|
|
231
|
+
const argv = process.argv.slice(2);
|
|
232
|
+
const opts = parseArgs(argv);
|
|
233
|
+
const cmd = opts._[0] || (opts.version ? 'version' : opts.help ? 'help' : 'help');
|
|
234
|
+
|
|
235
|
+
switch (cmd) {
|
|
236
|
+
case 'init': cmdInit(opts); break;
|
|
237
|
+
case 'uninstall': cmdUninstall(opts); break;
|
|
238
|
+
case 'doctor': cmdDoctor(); break;
|
|
239
|
+
case 'render': cmdRender(opts); break;
|
|
240
|
+
case 'watch': await cmdWatch(opts); break;
|
|
241
|
+
case 'version': console.log(PKG.version); break;
|
|
242
|
+
case 'help':
|
|
243
|
+
default: cmdHelp(); break;
|
|
244
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// moodline-core.mjs — engine da statusline (zero dependencias, ESM puro)
|
|
2
|
+
//
|
|
3
|
+
// Roda de dois jeitos:
|
|
4
|
+
// 1) Importado pelo bin/CLI (exporta render, adapters, DEFAULT_CFG).
|
|
5
|
+
// 2) Executado direto pela CLI de IA: le JSON no stdin, imprime a barra no stdout.
|
|
6
|
+
// Ex.: node moodline-core.mjs --adapter=claude --config=/caminho/config.json
|
|
7
|
+
//
|
|
8
|
+
// Este arquivo e copiado para ~/.claude/moodline/ (e ~/.copilot/moodline/) no `moodline init`,
|
|
9
|
+
// e o settings.json da CLI aponta direto pra ca. Mantido auto-contido de proposito: sem
|
|
10
|
+
// imports entre arquivos, so node built-ins, pra poder ser copiado sozinho e rodar rapido.
|
|
11
|
+
|
|
12
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
13
|
+
import { execFileSync } from 'node:child_process';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
|
|
16
|
+
// ---------------- ANSI ----------------
|
|
17
|
+
const ESC = '\x1b';
|
|
18
|
+
const RESET = `${ESC}[0m`;
|
|
19
|
+
const DIM = `${ESC}[2m`;
|
|
20
|
+
const CYAN = `${ESC}[36m`;
|
|
21
|
+
const MAGENTA = `${ESC}[35m`;
|
|
22
|
+
const GREEN = `${ESC}[32m`;
|
|
23
|
+
const RED = `${ESC}[31m`;
|
|
24
|
+
const YELLOW = `${ESC}[33m`;
|
|
25
|
+
const truecolor = (r, g, b) => `${ESC}[38;2;${r};${g};${b}m`;
|
|
26
|
+
|
|
27
|
+
// ---------------- config padrao ----------------
|
|
28
|
+
export const DEFAULT_CFG = {
|
|
29
|
+
layout: 'single', // 'single' | 'multi'
|
|
30
|
+
bar: { width: 20 },
|
|
31
|
+
punRotateMs: 30000, // troca o trocadilho a cada ~30s
|
|
32
|
+
features: {
|
|
33
|
+
git: true, // branch + estado (dirty/ahead/behind)
|
|
34
|
+
cost: true, // custo USD + tempo + linhas +/-
|
|
35
|
+
rate: true, // rate limits 5h/7d (Claude Pro/Max)
|
|
36
|
+
puns: true, // trocadilhos rotativos de dev
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ---------------- gradiente HSL (verde 120 -> vermelho 0) ----------------
|
|
41
|
+
function gradColor(pct) {
|
|
42
|
+
let h = (120 * (100 - pct)) / 100;
|
|
43
|
+
if (h < 0) h = 0;
|
|
44
|
+
if (h > 120) h = 120;
|
|
45
|
+
let r, g, b;
|
|
46
|
+
if (h < 60) { r = 255; g = (255 * h) / 60; b = 0; }
|
|
47
|
+
else { r = (255 * (120 - h)) / 60; g = 255; b = 0; }
|
|
48
|
+
return truecolor(Math.round(r), Math.round(g), Math.round(b));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------- barra (cheio / borda / vazio) ----------------
|
|
52
|
+
function bar(pct, width = 20) {
|
|
53
|
+
let full = Math.floor((pct * width) / 100);
|
|
54
|
+
if (full > width) full = width;
|
|
55
|
+
let edge = 0;
|
|
56
|
+
if (full < width && pct > 0) {
|
|
57
|
+
edge = 2;
|
|
58
|
+
if (full + edge > width) edge = width - full;
|
|
59
|
+
}
|
|
60
|
+
const empty = Math.max(0, width - full - edge);
|
|
61
|
+
return '█'.repeat(full) + '▒'.repeat(edge) + '░'.repeat(empty);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------- emoji-humor pela ocupacao ----------------
|
|
65
|
+
function mood(p) {
|
|
66
|
+
if (p >= 90) return '\u{1F480}'; // caveira
|
|
67
|
+
if (p >= 75) return '\u{1F975}'; // rosto quente
|
|
68
|
+
if (p >= 50) return '\u{1F605}'; // suando
|
|
69
|
+
if (p >= 25) return '\u{1F642}'; // sorriso leve
|
|
70
|
+
return '\u{1F60E}'; // de boa
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------- trocadilhos de dev (PT-BR) ----------------
|
|
74
|
+
const PUNS = [
|
|
75
|
+
'git push? mais pra git shove',
|
|
76
|
+
'funciona na minha maquina ¯\\_(ツ)_/¯',
|
|
77
|
+
'bug? feature nao documentada',
|
|
78
|
+
'merge sem conflito e merge mentiroso',
|
|
79
|
+
'CSS: Continua Sem Solucao',
|
|
80
|
+
'404: piada nao encontrada',
|
|
81
|
+
'deploy na sexta, coragem de sobra',
|
|
82
|
+
'cache: o problema e a solucao',
|
|
83
|
+
'recursao: ver "recursao"',
|
|
84
|
+
'prod de pe? reza o commit',
|
|
85
|
+
'TODO: escrever um TODO melhor',
|
|
86
|
+
'semicolon ; o heroi anonimo',
|
|
87
|
+
'rebase com fe, push com medo',
|
|
88
|
+
'git blame aponta pra mim de novo',
|
|
89
|
+
'tabs vs spaces: a guerra eterna',
|
|
90
|
+
'commit -m "ajustes"',
|
|
91
|
+
'tava funcionando ontem',
|
|
92
|
+
'dark mode pra esconder os bugs',
|
|
93
|
+
];
|
|
94
|
+
function pickPun(rotateMs) {
|
|
95
|
+
const idx = Math.floor(Date.now() / Math.max(1000, rotateMs)) % PUNS.length;
|
|
96
|
+
return PUNS[idx];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------- formatadores ----------------
|
|
100
|
+
const ktok = (t) => `${Math.round((Number(t) || 0) / 1000)}k`;
|
|
101
|
+
function money(usd) {
|
|
102
|
+
if (usd == null) return null;
|
|
103
|
+
const n = Number(usd);
|
|
104
|
+
if (!isFinite(n)) return null;
|
|
105
|
+
const dec = n > 0 && n < 0.01 ? 3 : 2; // mostra centavos de centavo so quando minusculo
|
|
106
|
+
return '$' + n.toFixed(dec);
|
|
107
|
+
}
|
|
108
|
+
function dur(ms) {
|
|
109
|
+
if (ms == null) return null;
|
|
110
|
+
const s = Math.floor(Number(ms) / 1000);
|
|
111
|
+
if (!isFinite(s) || s < 0) return null;
|
|
112
|
+
if (s < 60) return `${s}s`;
|
|
113
|
+
const m = Math.floor(s / 60);
|
|
114
|
+
if (m < 60) return `${m}m`;
|
|
115
|
+
const h = Math.floor(m / 60);
|
|
116
|
+
return `${h}h${m % 60}m`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------- git ----------------
|
|
120
|
+
function gitInfo(cwd) {
|
|
121
|
+
if (!cwd || !existsSync(cwd)) return null;
|
|
122
|
+
const opt = { cwd, encoding: 'utf8', timeout: 250, stdio: ['ignore', 'pipe', 'ignore'] };
|
|
123
|
+
try {
|
|
124
|
+
const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], opt).trim();
|
|
125
|
+
if (!branch) return null;
|
|
126
|
+
let dirty = false, ahead = 0, behind = 0;
|
|
127
|
+
try { dirty = execFileSync('git', ['status', '--porcelain'], opt).trim().length > 0; } catch {}
|
|
128
|
+
try {
|
|
129
|
+
const lr = execFileSync('git', ['rev-list', '--left-right', '--count', '@{u}...HEAD'], opt)
|
|
130
|
+
.trim().split(/\s+/);
|
|
131
|
+
behind = Number(lr[0]) || 0;
|
|
132
|
+
ahead = Number(lr[1]) || 0;
|
|
133
|
+
} catch {}
|
|
134
|
+
return { branch, dirty, ahead, behind };
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------- medir largura visivel (ignora ANSI; emoji conta 2) ----------------
|
|
141
|
+
function vlen(s) {
|
|
142
|
+
const t = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
143
|
+
let n = 0;
|
|
144
|
+
for (const ch of t) n += ch.codePointAt(0) > 0xffff ? 2 : 1;
|
|
145
|
+
return n;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------- render ----------------
|
|
149
|
+
export function render(state, cfg = DEFAULT_CFG) {
|
|
150
|
+
const f = cfg.features || {};
|
|
151
|
+
const cols = cfg.width || parseInt(process.env.COLUMNS || '', 10) || 80;
|
|
152
|
+
const c = gradColor(state.pct);
|
|
153
|
+
|
|
154
|
+
// segmento principal (sempre presente): modelo + effort + barra + emoji + pct + tokens
|
|
155
|
+
let core = `${CYAN}${state.model}${RESET}`;
|
|
156
|
+
if (state.effort) core += ` ${DIM}${state.effort}${RESET}`;
|
|
157
|
+
core += ` ${c}[${bar(state.pct, cfg.bar?.width ?? 20)}]${RESET}`;
|
|
158
|
+
core += ` ${mood(state.pct)} ${c}${state.pct}%${RESET} ${DIM}${ktok(state.tokens)}${RESET}`;
|
|
159
|
+
|
|
160
|
+
// segmentos opcionais (prioridade menor = sai por ultimo quando o terminal e estreito)
|
|
161
|
+
const opt = [];
|
|
162
|
+
|
|
163
|
+
if (f.git && state.git && state.git.branch) {
|
|
164
|
+
let g = `${MAGENTA}\u{1F33F} ${state.git.branch}${RESET}`;
|
|
165
|
+
const flags = [];
|
|
166
|
+
if (state.git.dirty) flags.push(`${YELLOW}*${RESET}`);
|
|
167
|
+
if (state.git.ahead) flags.push(`${GREEN}↑${state.git.ahead}${RESET}`);
|
|
168
|
+
if (state.git.behind) flags.push(`${RED}↓${state.git.behind}${RESET}`);
|
|
169
|
+
if (flags.length) g += ' ' + flags.join('');
|
|
170
|
+
opt.push({ txt: g, prio: 1 });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (f.cost) {
|
|
174
|
+
const parts = [];
|
|
175
|
+
const mo = money(state.costUsd);
|
|
176
|
+
if (mo) parts.push(`\u{1F4B8} ${mo}`);
|
|
177
|
+
const d = dur(state.durationMs);
|
|
178
|
+
if (d) parts.push(`⏱ ${d}`);
|
|
179
|
+
if (state.linesAdded != null || state.linesRemoved != null) {
|
|
180
|
+
parts.push(`${GREEN}+${state.linesAdded || 0}${RESET}/${RED}-${state.linesRemoved || 0}${RESET}`);
|
|
181
|
+
}
|
|
182
|
+
if (parts.length) opt.push({ txt: parts.join(' '), prio: 2 });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (f.rate && state.rate && (state.rate.five != null || state.rate.seven != null)) {
|
|
186
|
+
const parts = [];
|
|
187
|
+
if (state.rate.five != null) {
|
|
188
|
+
const v = Math.round(state.rate.five);
|
|
189
|
+
parts.push(`${gradColor(v)}5h ${v}%${RESET}`);
|
|
190
|
+
}
|
|
191
|
+
if (state.rate.seven != null) {
|
|
192
|
+
const v = Math.round(state.rate.seven);
|
|
193
|
+
parts.push(`${gradColor(v)}7d ${v}%${RESET}`);
|
|
194
|
+
}
|
|
195
|
+
opt.push({ txt: `⏳ ${parts.join(' ')}`, prio: 3 });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (f.puns) {
|
|
199
|
+
opt.push({ txt: `${DIM}\u{1F4AC} ${pickPun(cfg.punRotateMs ?? 30000)}${RESET}`, prio: 4 });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const sep = ` ${DIM}·${RESET} `;
|
|
203
|
+
const sepLen = 3;
|
|
204
|
+
|
|
205
|
+
if (cfg.layout === 'multi') {
|
|
206
|
+
const line2 = opt.sort((a, b) => a.prio - b.prio).map((s) => s.txt).join(sep);
|
|
207
|
+
return line2 ? `${core}\n${line2}` : core;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// single: encaixa o que couber na largura, dropando por prioridade
|
|
211
|
+
let out = core;
|
|
212
|
+
let used = vlen(core);
|
|
213
|
+
for (const s of opt.sort((a, b) => a.prio - b.prio)) {
|
|
214
|
+
const add = sepLen + vlen(s.txt);
|
|
215
|
+
if (used + add <= cols - 1) { out += sep + s.txt; used += add; }
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------- adapters (entrada bruta -> estado normalizado) ----------------
|
|
221
|
+
export function attachGit(state, useGit) {
|
|
222
|
+
if (state.gitBranch) {
|
|
223
|
+
state.git = { branch: state.gitBranch, dirty: false, ahead: 0, behind: 0 };
|
|
224
|
+
} else if (useGit) {
|
|
225
|
+
state.git = gitInfo(state.cwd);
|
|
226
|
+
} else {
|
|
227
|
+
state.git = null;
|
|
228
|
+
}
|
|
229
|
+
return state;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Claude Code — schema oficial (code.claude.com/docs/en/statusline)
|
|
233
|
+
export function fromClaude(j) {
|
|
234
|
+
const cw = j.context_window || {};
|
|
235
|
+
const rl = j.rate_limits || null;
|
|
236
|
+
return {
|
|
237
|
+
model: j.model?.display_name || '?',
|
|
238
|
+
effort: j.effort?.level || null,
|
|
239
|
+
pct: Math.floor(Number(cw.used_percentage) || 0),
|
|
240
|
+
tokens: Number(cw.total_input_tokens) || 0,
|
|
241
|
+
ctxSize: cw.context_window_size || null,
|
|
242
|
+
costUsd: j.cost?.total_cost_usd ?? null,
|
|
243
|
+
durationMs: j.cost?.total_duration_ms ?? null,
|
|
244
|
+
linesAdded: j.cost?.total_lines_added ?? null,
|
|
245
|
+
linesRemoved: j.cost?.total_lines_removed ?? null,
|
|
246
|
+
rate: rl ? { five: rl.five_hour?.used_percentage ?? null, seven: rl.seven_day?.used_percentage ?? null } : null,
|
|
247
|
+
cwd: j.workspace?.current_dir || j.cwd || null,
|
|
248
|
+
gitBranch: null, // Claude nao manda branch; calculamos via git
|
|
249
|
+
repo: j.workspace?.repo || null,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// GitHub Copilot CLI — schema espelha o do Claude Code (statusLine experimental)
|
|
254
|
+
export function fromCopilot(j) {
|
|
255
|
+
const cw = j.context_window || {};
|
|
256
|
+
return {
|
|
257
|
+
model: j.model?.display_name || '?',
|
|
258
|
+
effort: j.effort?.level || j.model?.effort || null,
|
|
259
|
+
pct: Math.floor(Number(cw.used_percentage) || 0),
|
|
260
|
+
tokens: Number(cw.total_input_tokens ?? cw.current_context_tokens) || 0,
|
|
261
|
+
ctxSize: cw.context_window_size || cw.displayed_context_limit || null,
|
|
262
|
+
costUsd: j.cost?.total_cost_usd ?? null,
|
|
263
|
+
durationMs: j.cost?.total_duration_ms ?? null,
|
|
264
|
+
linesAdded: j.cost?.total_lines_added ?? null,
|
|
265
|
+
linesRemoved: j.cost?.total_lines_removed ?? null,
|
|
266
|
+
rate: null,
|
|
267
|
+
cwd: j.cwd || null,
|
|
268
|
+
gitBranch: j.remote?.branch || null,
|
|
269
|
+
repo: null,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// OpenCode — EXPERIMENTAL. Sem statusLine nativa; alimentado pelo `moodline watch` (HTTP/SSE).
|
|
274
|
+
// Mapeamento tolerante; ajuste conforme a versao da API do OpenCode.
|
|
275
|
+
export function fromOpenCode(j) {
|
|
276
|
+
const ctx = j.context || j.tokens || {};
|
|
277
|
+
return {
|
|
278
|
+
model: j.model?.name || j.model?.id || j.model || '?',
|
|
279
|
+
effort: null,
|
|
280
|
+
pct: Math.floor(Number(ctx.used_percentage ?? ctx.percentage ?? j.percentage) || 0),
|
|
281
|
+
tokens: Number(ctx.input ?? ctx.tokens ?? j.input_tokens) || 0,
|
|
282
|
+
ctxSize: ctx.limit || null,
|
|
283
|
+
costUsd: j.cost ?? null,
|
|
284
|
+
durationMs: null,
|
|
285
|
+
linesAdded: null,
|
|
286
|
+
linesRemoved: null,
|
|
287
|
+
rate: null,
|
|
288
|
+
cwd: j.directory || j.cwd || null,
|
|
289
|
+
gitBranch: j.git?.branch || null,
|
|
290
|
+
repo: null,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Gemini CLI — EXPERIMENTAL. Sem statusLine nativa por comando; passthroughgenerico de JSON
|
|
295
|
+
// caso o usuario ligue via hook/extensao. Veja docs do projeto.
|
|
296
|
+
export function fromGemini(j) {
|
|
297
|
+
const cw = j.context_window || j.context || {};
|
|
298
|
+
return {
|
|
299
|
+
model: j.model?.display_name || j.model || '?',
|
|
300
|
+
effort: null,
|
|
301
|
+
pct: Math.floor(Number(cw.used_percentage ?? j.percentage) || 0),
|
|
302
|
+
tokens: Number(cw.total_input_tokens ?? cw.tokens) || 0,
|
|
303
|
+
ctxSize: cw.context_window_size || null,
|
|
304
|
+
costUsd: null,
|
|
305
|
+
durationMs: null,
|
|
306
|
+
linesAdded: null,
|
|
307
|
+
linesRemoved: null,
|
|
308
|
+
rate: null,
|
|
309
|
+
cwd: j.cwd || null,
|
|
310
|
+
gitBranch: null,
|
|
311
|
+
repo: null,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export const ADAPTERS = {
|
|
316
|
+
claude: fromClaude,
|
|
317
|
+
copilot: fromCopilot,
|
|
318
|
+
opencode: fromOpenCode,
|
|
319
|
+
gemini: fromGemini,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// ---------------- util ----------------
|
|
323
|
+
function deepMerge(base, over) {
|
|
324
|
+
const out = { ...base };
|
|
325
|
+
for (const k of Object.keys(over || {})) {
|
|
326
|
+
if (over[k] && typeof over[k] === 'object' && !Array.isArray(over[k])) {
|
|
327
|
+
out[k] = deepMerge(base[k] || {}, over[k]);
|
|
328
|
+
} else {
|
|
329
|
+
out[k] = over[k];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function loadConfig(path) {
|
|
336
|
+
let cfg = structuredClone(DEFAULT_CFG);
|
|
337
|
+
if (path && existsSync(path)) {
|
|
338
|
+
try { cfg = deepMerge(cfg, JSON.parse(readFileSync(path, 'utf8'))); } catch {}
|
|
339
|
+
}
|
|
340
|
+
return cfg;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function parseArgs(argv) {
|
|
344
|
+
const o = { _: [] };
|
|
345
|
+
for (const a of argv) {
|
|
346
|
+
if (a.startsWith('--')) {
|
|
347
|
+
const [k, v] = a.slice(2).split('=');
|
|
348
|
+
o[k] = v === undefined ? true : v;
|
|
349
|
+
} else o._.push(a);
|
|
350
|
+
}
|
|
351
|
+
return o;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ---------------- main (so quando executado direto) ----------------
|
|
355
|
+
function readStdin() {
|
|
356
|
+
if (process.stdin.isTTY) return '';
|
|
357
|
+
try { return readFileSync(0, 'utf8'); } catch { return ''; }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function runMain(argv) {
|
|
361
|
+
const args = parseArgs(argv);
|
|
362
|
+
const adapterName = (args.adapter || 'claude').toLowerCase();
|
|
363
|
+
const adapter = ADAPTERS[adapterName] || fromClaude;
|
|
364
|
+
|
|
365
|
+
const cfg = loadConfig(args.config);
|
|
366
|
+
if (args['no-git']) cfg.features.git = false;
|
|
367
|
+
if (args['no-cost']) cfg.features.cost = false;
|
|
368
|
+
if (args['no-rate']) cfg.features.rate = false;
|
|
369
|
+
if (args['no-puns']) cfg.features.puns = false;
|
|
370
|
+
if (args.multi) cfg.layout = 'multi';
|
|
371
|
+
if (args.width) cfg.width = parseInt(args.width, 10) || undefined;
|
|
372
|
+
|
|
373
|
+
let raw = {};
|
|
374
|
+
try { raw = JSON.parse(readStdin() || '{}'); } catch { raw = {}; }
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const state = attachGit(adapter(raw), cfg.features.git);
|
|
378
|
+
process.stdout.write(render(state, cfg) + '\n');
|
|
379
|
+
} catch (e) {
|
|
380
|
+
// statusline nunca deve quebrar a CLI: imprime algo minimo
|
|
381
|
+
process.stdout.write(`${CYAN}${raw?.model?.display_name || 'moodline'}${RESET}\n`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
386
|
+
if (isMain) runMain(process.argv.slice(2));
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "moodline",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Statusline divertida e informativa para CLIs de IA (Claude Code, GitHub Copilot CLI e mais): barra de contexto em gradiente, emoji-humor, git, custo e trocadilhos de dev.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"moodline": "bin/moodline.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./lib/moodline-core.mjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"lib",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node test/smoke.mjs"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"statusline",
|
|
26
|
+
"status-line",
|
|
27
|
+
"claude-code",
|
|
28
|
+
"copilot-cli",
|
|
29
|
+
"github-copilot",
|
|
30
|
+
"ai-cli",
|
|
31
|
+
"cli",
|
|
32
|
+
"terminal",
|
|
33
|
+
"prompt",
|
|
34
|
+
"context-window",
|
|
35
|
+
"tokens",
|
|
36
|
+
"emoji",
|
|
37
|
+
"developer-tools"
|
|
38
|
+
],
|
|
39
|
+
"author": "Alison Amorim <alison.amorim.mail@gmail.com>",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/slipalison/moodline.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/slipalison/moodline#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/slipalison/moodline/issues"
|
|
48
|
+
}
|
|
49
|
+
}
|