arsenal-agent 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/COMANDOS.md ADDED
@@ -0,0 +1,34 @@
1
+ # arsenal-agent — Comandos
2
+
3
+ ## Trocar de conta
4
+
5
+ ```bash
6
+ aa profile use pessoal # dbezerra106@gmail.com
7
+ aa profile use trabalho # claude-datascience@zerum.com
8
+ ```
9
+
10
+ ## Ver contas cadastradas
11
+
12
+ ```bash
13
+ aa profile list
14
+ ```
15
+
16
+ ## Abrir o TUI (dual agent)
17
+
18
+ ```bash
19
+ aa # usa o perfil ativo
20
+ aa --profile trabalho # força um perfil específico
21
+ ```
22
+
23
+ ## Adicionar nova conta
24
+
25
+ ```bash
26
+ aa profile add <nome>
27
+ ```
28
+
29
+ ## Onde ficam os tokens
30
+
31
+ ```
32
+ ~/.arsenal-agent/credentials/pessoal.json
33
+ ~/.arsenal-agent/credentials/trabalho.json
34
+ ```
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # arsenal-agent
2
+
3
+ Dual AI agent CLI — roteamento automático entre Claude Sonnet e Nemotron (free).
4
+
5
+ Tasks simples vão pro Nemotron via OpenCode (grátis). Tasks complexas vão pro Claude Sonnet.
6
+
7
+ ## Instalação
8
+
9
+ ```bash
10
+ npm install -g arsenal-agent
11
+ ```
12
+
13
+ ## Uso
14
+
15
+ ```bash
16
+ arsenal-agent
17
+ # ou o alias curto:
18
+ aa
19
+ ```
20
+
21
+ ## Requisitos
22
+
23
+ - `opencode` instalado: `npm install -g opencode-ai`
24
+ - `ANTHROPIC_API_KEY` no ambiente
25
+ - `OPENROUTER_API_KEY` no ambiente
26
+
27
+ ## Como funciona
28
+
29
+ ```
30
+ Você digita uma task
31
+ └── ROUTER classifica
32
+ ├── task simples → Nemotron 120B (OpenRouter, free)
33
+ └── task complexa → Claude Sonnet 4.6 (Anthropic)
34
+ ```
35
+
36
+ Teclado: `ctrl+c` sair · `ctrl+l` limpar
package/app.py ADDED
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dual Agent CLI — Claude Code + OpenCode side-by-side
4
+ Roteador automatico: tasks simples vao pro Nemotron (free), complexas pro Claude Sonnet.
5
+ """
6
+
7
+ import subprocess
8
+ import threading
9
+ import time
10
+ import os
11
+ import re
12
+ from datetime import datetime
13
+ from textual.app import App, ComposeResult
14
+ from textual.containers import Horizontal, Vertical
15
+ from textual.widgets import (
16
+ Header, Footer, Input, RichLog, Static, Label
17
+ )
18
+ from textual.reactive import reactive
19
+ from textual import work
20
+ from rich.text import Text
21
+ from rich.panel import Panel
22
+
23
+ # ── Custo aproximado por 1k tokens ──────────────────────────────────────────
24
+ COST_PER_1K = {
25
+ "nemotron-120b": 0.0,
26
+ "nemotron-nano": 0.0,
27
+ "sonnet": 0.003,
28
+ }
29
+
30
+ MODELS = {
31
+ "nemotron-120b": "openrouter/nvidia/nemotron-3-super-120b-a12b:free",
32
+ "nemotron-nano": "openrouter/nvidia/nemotron-nano-12b-v2-vl:free",
33
+ "sonnet": "anthropic/claude-sonnet-4-6",
34
+ }
35
+
36
+ # Palavras-chave que indicam task COMPLEXA → Claude Sonnet
37
+ COMPLEX_KEYWORDS = [
38
+ "arquitetura", "architecture", "seguranca", "security", "vulnerabilidade",
39
+ "refatora", "refactor", "por que", "why", "debug", "erro complexo",
40
+ "analisa", "analyze", "projeta", "design", "mcp", "agente", "agent",
41
+ "pipeline", "infraestrutura", "infrastructure", "revisao", "review",
42
+ "estrategia", "strategy", "implemente", "implement", "crie um sistema",
43
+ "como funciona", "how does", "explique a arquitetura",
44
+ ]
45
+
46
+
47
+ def classify_task(task: str) -> tuple[str, str]:
48
+ """Retorna (model_alias, razao) para a task."""
49
+ low = task.lower()
50
+ for kw in COMPLEX_KEYWORDS:
51
+ if kw in low:
52
+ return "sonnet", f"keyword: '{kw}'"
53
+ if len(task) > 400:
54
+ return "sonnet", "prompt longo (>400 chars)"
55
+ return "nemotron-120b", "task simples → free"
56
+
57
+
58
+ def run_opencode(task: str, model_alias: str) -> tuple[str, float]:
59
+ """Executa opencode run e retorna (output, tempo_segundos)."""
60
+ model_id = MODELS[model_alias]
61
+ t0 = time.time()
62
+ result = subprocess.run(
63
+ ["opencode", "run", task, "--model", model_id],
64
+ capture_output=True, text=True, timeout=120,
65
+ )
66
+ elapsed = time.time() - t0
67
+ output = result.stdout.strip() or result.stderr.strip() or "Sem resposta."
68
+ # Remove escape sequences de cor do opencode
69
+ output = re.sub(r'\x1b\[[0-9;]*m', '', output)
70
+ output = re.sub(r'\[[\d]+m', '', output)
71
+ return output.strip(), elapsed
72
+
73
+
74
+ def run_claude(task: str) -> tuple[str, float]:
75
+ """Executa Claude via claude CLI e retorna (output, tempo_segundos)."""
76
+ t0 = time.time()
77
+ result = subprocess.run(
78
+ ["claude", "--print", task],
79
+ capture_output=True, text=True, timeout=180,
80
+ env={**os.environ},
81
+ )
82
+ elapsed = time.time() - t0
83
+ output = result.stdout.strip() or result.stderr.strip() or "Sem resposta do Claude."
84
+ return output, elapsed
85
+
86
+
87
+ # ── Widgets ─────────────────────────────────────────────────────────────────
88
+
89
+ class AgentPanel(Vertical):
90
+ """Painel de um agente com log de mensagens."""
91
+
92
+ DEFAULT_CSS = """
93
+ AgentPanel {
94
+ border: solid $primary;
95
+ padding: 0 1;
96
+ height: 100%;
97
+ }
98
+ AgentPanel .panel-title {
99
+ text-align: center;
100
+ text-style: bold;
101
+ color: $accent;
102
+ padding: 0 0 1 0;
103
+ }
104
+ AgentPanel RichLog {
105
+ height: 1fr;
106
+ }
107
+ """
108
+
109
+ def __init__(self, title: str, id: str):
110
+ super().__init__(id=id)
111
+ self._title = title
112
+
113
+ def compose(self) -> ComposeResult:
114
+ yield Label(self._title, classes="panel-title")
115
+ yield RichLog(highlight=True, markup=True, wrap=True, id=f"{self.id}-log")
116
+
117
+ def log(self, text: str, style: str = ""):
118
+ log = self.query_one(f"#{self.id}-log", RichLog)
119
+ if style:
120
+ log.write(Text(text, style=style))
121
+ else:
122
+ log.write(text)
123
+
124
+ def clear(self):
125
+ self.query_one(f"#{self.id}-log", RichLog).clear()
126
+
127
+
128
+ class StatsBar(Static):
129
+ """Barra de estatísticas de tokens/custo."""
130
+
131
+ tasks_routed: reactive[int] = reactive(0)
132
+ tokens_saved: reactive[int] = reactive(0)
133
+ cost_paid: reactive[float] = reactive(0.0)
134
+ cost_saved: reactive[float] = reactive(0.0)
135
+
136
+ DEFAULT_CSS = """
137
+ StatsBar {
138
+ background: $surface;
139
+ padding: 0 2;
140
+ height: 3;
141
+ border-top: solid $primary-darken-2;
142
+ color: $text-muted;
143
+ }
144
+ """
145
+
146
+ def render(self) -> str:
147
+ saved_pct = 0
148
+ total = self.cost_paid + self.cost_saved
149
+ if total > 0:
150
+ saved_pct = int((self.cost_saved / total) * 100)
151
+ return (
152
+ f" Tasks: [bold]{self.tasks_routed}[/bold] | "
153
+ f"Economizado: [green]{saved_pct}%[/green] (~${self.cost_saved:.4f}) | "
154
+ f"Gasto real: [yellow]${self.cost_paid:.4f}[/yellow] | "
155
+ f"[dim]Nemotron = free • Sonnet = $3/MTok[/dim]"
156
+ )
157
+
158
+
159
+ # ── App principal ────────────────────────────────────────────────────────────
160
+
161
+ class DualAgentApp(App):
162
+ """Dual Agent CLI: Claude Code + OpenCode side-by-side."""
163
+
164
+ CSS = """
165
+ Screen {
166
+ background: #0a0a0a;
167
+ }
168
+ Header {
169
+ background: #111111;
170
+ }
171
+ Footer {
172
+ background: #111111;
173
+ }
174
+ #main {
175
+ height: 1fr;
176
+ }
177
+ #left-panel {
178
+ width: 1fr;
179
+ border: solid #00aaff;
180
+ }
181
+ #router-panel {
182
+ width: 28;
183
+ border: solid #444444;
184
+ padding: 0 1;
185
+ height: 100%;
186
+ }
187
+ #router-panel Label {
188
+ text-align: center;
189
+ color: #888888;
190
+ text-style: bold;
191
+ padding: 0 0 1 0;
192
+ }
193
+ #router-log {
194
+ height: 1fr;
195
+ color: #666666;
196
+ }
197
+ #right-panel {
198
+ width: 1fr;
199
+ border: solid #ff6600;
200
+ }
201
+ #input-bar {
202
+ height: 3;
203
+ padding: 0 1;
204
+ border-top: solid #222222;
205
+ }
206
+ #task-input {
207
+ width: 1fr;
208
+ }
209
+ """
210
+
211
+ TITLE = "Dual Agent CLI"
212
+ SUB_TITLE = "Claude Sonnet + Nemotron (free)"
213
+ BINDINGS = [
214
+ ("ctrl+c", "quit", "Sair"),
215
+ ("ctrl+l", "clear_all", "Limpar"),
216
+ ]
217
+
218
+ def __init__(self):
219
+ super().__init__()
220
+ self._stats = None
221
+
222
+ def compose(self) -> ComposeResult:
223
+ yield Header()
224
+ with Horizontal(id="main"):
225
+ yield AgentPanel("Claude Sonnet 4.6 [dim](tasks complexas)[/dim]", id="left-panel")
226
+ with Vertical(id="router-panel"):
227
+ yield Label("ROUTER")
228
+ yield RichLog(highlight=False, markup=True, wrap=True, id="router-log")
229
+ yield AgentPanel("Nemotron 120B [dim](free • tasks simples)[/dim]", id="right-panel")
230
+ with Horizontal(id="input-bar"):
231
+ yield Input(placeholder="Digite sua task e pressione Enter...", id="task-input")
232
+ yield StatsBar(id="stats")
233
+ yield Footer()
234
+
235
+ def on_mount(self) -> None:
236
+ self._stats = self.query_one("#stats", StatsBar)
237
+ self.query_one("#task-input", Input).focus()
238
+ self._router_log("Sistema iniciado")
239
+ self._router_log(f"Claude: claude CLI")
240
+ self._router_log(f"OpenCode: {MODELS['nemotron-120b'][:30]}...")
241
+
242
+ def _router_log(self, msg: str) -> None:
243
+ ts = datetime.now().strftime("%H:%M:%S")
244
+ log = self.query_one("#router-log", RichLog)
245
+ log.write(f"[dim]{ts}[/dim] {msg}")
246
+
247
+ def on_input_submitted(self, event: Input.Submitted) -> None:
248
+ task = event.value.strip()
249
+ if not task:
250
+ return
251
+ event.input.value = ""
252
+ self._dispatch_task(task)
253
+
254
+ @work(thread=True)
255
+ def _dispatch_task(self, task: str) -> None:
256
+ model_alias, reason = classify_task(task)
257
+ ts = datetime.now().strftime("%H:%M:%S")
258
+
259
+ self.call_from_thread(self._router_log, f"[cyan]→[/cyan] {task[:40]}...")
260
+ self.call_from_thread(self._router_log, f" [yellow]{model_alias}[/yellow] ({reason})")
261
+
262
+ if model_alias == "sonnet":
263
+ panel = self.query_one("#left-panel", AgentPanel)
264
+ self.call_from_thread(panel.log, f"\n[bold cyan][{ts}] Você:[/bold cyan] {task}")
265
+ self.call_from_thread(panel.log, "[dim]aguardando Claude...[/dim]", "dim")
266
+
267
+ output, elapsed = run_claude(task)
268
+
269
+ self.call_from_thread(panel.log, f"[bold green]Claude:[/bold green]")
270
+ self.call_from_thread(panel.log, output)
271
+ self.call_from_thread(panel.log, f"[dim]({elapsed:.1f}s)[/dim]")
272
+
273
+ # Estima custo: ~300 tokens de output
274
+ cost = (300 / 1000) * COST_PER_1K["sonnet"]
275
+ cost_saved = (300 / 1000) * COST_PER_1K["nemotron-120b"] # seria 0
276
+ self.call_from_thread(self._update_stats, cost, 0.0)
277
+
278
+ else:
279
+ panel = self.query_one("#right-panel", AgentPanel)
280
+ self.call_from_thread(panel.log, f"\n[bold orange1][{ts}] Você:[/bold orange1] {task}")
281
+ self.call_from_thread(panel.log, "[dim]aguardando Nemotron...[/dim]", "dim")
282
+
283
+ output, elapsed = run_opencode(task, model_alias)
284
+
285
+ self.call_from_thread(panel.log, f"[bold green]Nemotron:[/bold green]")
286
+ self.call_from_thread(panel.log, output)
287
+ self.call_from_thread(panel.log, f"[dim]({elapsed:.1f}s • $0.00)[/dim]")
288
+
289
+ # Quanto teria custado no Sonnet
290
+ cost_saved = (300 / 1000) * COST_PER_1K["sonnet"]
291
+ self.call_from_thread(self._update_stats, 0.0, cost_saved)
292
+
293
+ def _update_stats(self, cost_paid: float, cost_saved: float) -> None:
294
+ stats = self._stats
295
+ stats.tasks_routed += 1
296
+ stats.cost_paid += cost_paid
297
+ stats.cost_saved += cost_saved
298
+
299
+ def action_clear_all(self) -> None:
300
+ self.query_one("#left-panel", AgentPanel).clear()
301
+ self.query_one("#right-panel", AgentPanel).clear()
302
+ self.query_one("#router-log", RichLog).clear()
303
+ self._router_log("Logs limpos")
304
+
305
+
306
+ if __name__ == "__main__":
307
+ app = DualAgentApp()
308
+ app.run()
@@ -0,0 +1,8 @@
1
+ import type { ModelAlias } from "./types.js";
2
+ export interface AgentResult {
3
+ content: string;
4
+ elapsed: number;
5
+ model: ModelAlias;
6
+ }
7
+ export declare function runClaude(task: string): Promise<AgentResult>;
8
+ export declare function runOpencode(task: string, alias: ModelAlias): Promise<AgentResult>;
package/dist/agents.js ADDED
@@ -0,0 +1,37 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import Anthropic from "@anthropic-ai/sdk";
4
+ import { getModelId } from "./router.js";
5
+ const execFileAsync = promisify(execFile);
6
+ const anthropic = new Anthropic({
7
+ apiKey: process.env.ANTHROPIC_API_KEY,
8
+ });
9
+ export async function runClaude(task) {
10
+ const t0 = Date.now();
11
+ const message = await anthropic.messages.create({
12
+ model: "claude-sonnet-4-6-20251101",
13
+ max_tokens: 2048,
14
+ messages: [{ role: "user", content: task }],
15
+ });
16
+ const content = message.content
17
+ .filter((b) => b.type === "text")
18
+ .map((b) => b.text)
19
+ .join("\n");
20
+ return { content, elapsed: (Date.now() - t0) / 1000, model: "sonnet" };
21
+ }
22
+ export async function runOpencode(task, alias) {
23
+ const t0 = Date.now();
24
+ const modelId = getModelId(alias);
25
+ try {
26
+ const { stdout, stderr } = await execFileAsync("opencode", ["run", task, "--model", modelId], { timeout: 120_000, env: process.env });
27
+ const raw = stdout || stderr || "Sem resposta.";
28
+ // Remove ANSI escape codes
29
+ const content = raw.replace(/\x1b\[[0-9;]*m/g, "").replace(/\[[\d]+m/g, "").trim();
30
+ return { content, elapsed: (Date.now() - t0) / 1000, model: alias };
31
+ }
32
+ catch (err) {
33
+ const msg = err instanceof Error ? err.message : String(err);
34
+ return { content: `Erro: ${msg}`, elapsed: (Date.now() - t0) / 1000, model: alias };
35
+ }
36
+ }
37
+ //# sourceMappingURL=agents.js.map
package/dist/app.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function App(): import("react/jsx-runtime").JSX.Element;
package/dist/app.js ADDED
@@ -0,0 +1,121 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState, useCallback } from "react";
3
+ import { Box, Text, useInput, useApp } from "ink";
4
+ import { classifyTask, COST_PER_1K } from "./router.js";
5
+ import { runClaude, runOpencode } from "./agents.js";
6
+ // ── Componentes ──────────────────────────────────────────────────────────────
7
+ function MessageList({ messages, model }) {
8
+ const filtered = messages.filter((m) => model === "sonnet" ? m.model === "sonnet" : m.model !== "sonnet");
9
+ return (_jsx(Box, { flexDirection: "column", gap: 0, children: filtered.map((msg) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: msg.role === "user" ? "cyan" : "green", bold: true, children: [msg.role === "user" ? "Você" : model === "sonnet" ? "Claude" : "Nemotron", msg.elapsed ? ` (${msg.elapsed.toFixed(1)}s)` : ""] }), _jsx(Text, { wrap: "wrap", children: msg.content })] }, msg.id))) }));
10
+ }
11
+ function Panel({ title, color, messages, model, loading, width, }) {
12
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: color, width: width, height: 30, paddingX: 1, children: [_jsx(Text, { bold: true, color: color, children: title }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [_jsx(MessageList, { messages: messages, model: model }), loading && (_jsx(Text, { color: "gray", dimColor: true, children: "aguardando..." }))] })] }));
13
+ }
14
+ function RouterLog({ entries }) {
15
+ const last = entries.slice(-12);
16
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", width: 26, height: 30, paddingX: 1, children: [_jsx(Text, { bold: true, color: "gray", children: "ROUTER" }), last.map((e, i) => (_jsx(Text, { color: "gray", dimColor: true, wrap: "wrap", children: e }, i)))] }));
17
+ }
18
+ function StatsBar({ stats }) {
19
+ const pct = stats.costSaved + stats.costPaid > 0
20
+ ? Math.round((stats.costSaved / (stats.costSaved + stats.costPaid)) * 100)
21
+ : 0;
22
+ return (_jsx(Box, { borderStyle: "single", borderColor: "gray", paddingX: 2, marginTop: 0, children: _jsxs(Text, { children: ["Tasks: ", _jsx(Text, { bold: true, children: stats.tasksTotal }), " ", "Free: ", _jsx(Text, { color: "green", bold: true, children: stats.tasksFree }), " ", "Pago: ", _jsx(Text, { color: "yellow", bold: true, children: stats.tasksPaid }), " ", "Economizado: ", _jsxs(Text, { color: "green", bold: true, children: [pct, "%"] }), " (~$", stats.costSaved.toFixed(4), ")", " ", "Gasto: ", _jsxs(Text, { color: "yellow", children: ["$", stats.costPaid.toFixed(4)] }), " ", _jsx(Text, { dimColor: true, children: "[ctrl+c sair \u2022 ctrl+l limpar]" })] }) }));
23
+ }
24
+ // ── App principal ─────────────────────────────────────────────────────────────
25
+ export function App() {
26
+ const { exit } = useApp();
27
+ const [messages, setMessages] = useState([]);
28
+ const [routerLog, setRouterLog] = useState(["iniciado", "claude: sonnet-4-6", "free: nemotron-120b"]);
29
+ const [input, setInput] = useState("");
30
+ const [loadingLeft, setLoadingLeft] = useState(false);
31
+ const [loadingRight, setLoadingRight] = useState(false);
32
+ const [stats, setStats] = useState({
33
+ tasksTotal: 0, tasksFree: 0, tasksPaid: 0, costPaid: 0, costSaved: 0,
34
+ });
35
+ const addRouter = useCallback((line) => {
36
+ setRouterLog((prev) => [...prev, line]);
37
+ }, []);
38
+ const dispatch = useCallback(async (task) => {
39
+ const decision = classifyTask(task);
40
+ const ts = new Date().toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
41
+ addRouter(`${ts} → ${task.slice(0, 22)}...`);
42
+ addRouter(` ${decision.model} (${decision.reason})`);
43
+ const userMsg = {
44
+ id: `u-${Date.now()}`,
45
+ role: "user",
46
+ content: task,
47
+ model: decision.model,
48
+ timestamp: new Date(),
49
+ };
50
+ setMessages((prev) => [...prev, userMsg]);
51
+ if (decision.model === "sonnet") {
52
+ setLoadingLeft(true);
53
+ try {
54
+ const result = await runClaude(task);
55
+ const assistantMsg = {
56
+ id: `a-${Date.now()}`,
57
+ role: "assistant",
58
+ content: result.content,
59
+ model: "sonnet",
60
+ elapsed: result.elapsed,
61
+ timestamp: new Date(),
62
+ };
63
+ setMessages((prev) => [...prev, assistantMsg]);
64
+ const cost = (300 / 1000) * COST_PER_1K["sonnet"];
65
+ const saved = (300 / 1000) * COST_PER_1K["nemotron-120b"];
66
+ setStats((s) => ({ ...s, tasksTotal: s.tasksTotal + 1, tasksPaid: s.tasksPaid + 1, costPaid: s.costPaid + cost, costSaved: s.costSaved + saved }));
67
+ }
68
+ finally {
69
+ setLoadingLeft(false);
70
+ }
71
+ }
72
+ else {
73
+ setLoadingRight(true);
74
+ try {
75
+ const result = await runOpencode(task, decision.model);
76
+ const assistantMsg = {
77
+ id: `a-${Date.now()}`,
78
+ role: "assistant",
79
+ content: result.content,
80
+ model: decision.model,
81
+ elapsed: result.elapsed,
82
+ timestamp: new Date(),
83
+ };
84
+ setMessages((prev) => [...prev, assistantMsg]);
85
+ const saved = (300 / 1000) * COST_PER_1K["sonnet"];
86
+ setStats((s) => ({ ...s, tasksTotal: s.tasksTotal + 1, tasksFree: s.tasksFree + 1, costSaved: s.costSaved + saved }));
87
+ }
88
+ finally {
89
+ setLoadingRight(false);
90
+ }
91
+ }
92
+ }, [addRouter]);
93
+ useInput((char, key) => {
94
+ if (key.ctrl && char === "c") {
95
+ exit();
96
+ return;
97
+ }
98
+ if (key.ctrl && char === "l") {
99
+ setMessages([]);
100
+ setRouterLog(["limpo"]);
101
+ return;
102
+ }
103
+ if (key.return) {
104
+ const task = input.trim();
105
+ if (task) {
106
+ setInput("");
107
+ dispatch(task);
108
+ }
109
+ return;
110
+ }
111
+ if (key.backspace || key.delete) {
112
+ setInput((prev) => prev.slice(0, -1));
113
+ return;
114
+ }
115
+ if (char && !key.ctrl && !key.meta) {
116
+ setInput((prev) => prev + char);
117
+ }
118
+ });
119
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "blue", children: " arsenal-agent " }), _jsx(Text, { dimColor: true, children: "Claude Sonnet + Nemotron free \u2014 roteamento automatico" })] }), _jsxs(Box, { flexDirection: "row", gap: 0, children: [_jsx(Panel, { title: "Claude Sonnet 4.6 (tasks complexas)", color: "cyan", messages: messages, model: "sonnet", loading: loadingLeft, width: 50 }), _jsx(RouterLog, { entries: routerLog }), _jsx(Panel, { title: "Nemotron 120B (free \u00B7 tasks simples)", color: "yellow", messages: messages, model: "nemotron", loading: loadingRight, width: 50 })] }), _jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsxs(Text, { color: "gray", children: [">", " "] }), _jsx(Text, { children: input }), _jsx(Text, { color: "gray", children: "\u2588" })] }), _jsx(StatsBar, { stats: stats })] }));
120
+ }
121
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { render } from "ink";
4
+ import { Command } from "commander";
5
+ import { App } from "./app.js";
6
+ import { addProfile, listProfiles, useProfile, loadProfile, applyProfile } from "./profiles.js";
7
+ const program = new Command();
8
+ program
9
+ .name("arsenal-agent")
10
+ .description("Dual AI agent CLI — Claude Sonnet + Nemotron free, roteamento automatico")
11
+ .version("0.1.0")
12
+ .option("-p, --profile <name>", "perfil a usar (substitui env vars)");
13
+ // ── Subcomando: profile ────────────────────────────────────────────────────
14
+ const profileCmd = program
15
+ .command("profile")
16
+ .description("gerenciar perfis de API keys");
17
+ profileCmd
18
+ .command("add [name]")
19
+ .description("adicionar ou atualizar um perfil")
20
+ .action(async (name) => {
21
+ await addProfile(name);
22
+ process.exit(0);
23
+ });
24
+ profileCmd
25
+ .command("list")
26
+ .alias("ls")
27
+ .description("listar perfis cadastrados")
28
+ .action(() => {
29
+ listProfiles();
30
+ process.exit(0);
31
+ });
32
+ profileCmd
33
+ .command("use <name>")
34
+ .description("definir perfil ativo padrão")
35
+ .action((name) => {
36
+ useProfile(name);
37
+ process.exit(0);
38
+ });
39
+ // ── Ação principal: lança TUI ──────────────────────────────────────────────
40
+ program.action((opts) => {
41
+ // Aplica perfil se informado ou se há um perfil ativo salvo
42
+ const profile = loadProfile(opts.profile);
43
+ if (profile) {
44
+ applyProfile(profile);
45
+ process.stderr.write(`[arsenal-agent] perfil: ${profile.label ?? profile.name}\n`);
46
+ }
47
+ else if (!process.env.ANTHROPIC_API_KEY) {
48
+ console.error("Nenhuma ANTHROPIC_API_KEY encontrada.");
49
+ console.error("Adicione um perfil: aa profile add");
50
+ console.error("Ou exporte a variável: export ANTHROPIC_API_KEY=sk-ant-...");
51
+ process.exit(1);
52
+ }
53
+ const { waitUntilExit } = render(_jsx(App, {}));
54
+ waitUntilExit().then(() => process.exit(0));
55
+ });
56
+ program.parse();
57
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,14 @@
1
+ export type AuthMethod = "api_key" | "browser";
2
+ export interface Profile {
3
+ name: string;
4
+ label?: string;
5
+ auth_method: AuthMethod;
6
+ anthropic_api_key?: string;
7
+ openrouter_api_key?: string;
8
+ email?: string;
9
+ }
10
+ export declare function addProfile(name?: string): Promise<void>;
11
+ export declare function listProfiles(): void;
12
+ export declare function useProfile(name: string): void;
13
+ export declare function loadProfile(name?: string): Profile | null;
14
+ export declare function applyProfile(profile: Profile): void;