arsenal-agent 0.1.0 → 0.1.2

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/dist/app.js CHANGED
@@ -3,93 +3,77 @@ import { useState, useCallback } from "react";
3
3
  import { Box, Text, useInput, useApp } from "ink";
4
4
  import { classifyTask, COST_PER_1K } from "./router.js";
5
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))) }));
6
+ import { loadProfile } from "./profiles.js";
7
+ // ── Badge por modelo ─────────────────────────────────────────────────────────
8
+ const MODEL_BADGE = {
9
+ "sonnet": { label: "Claude Sonnet", color: "cyan" },
10
+ "nemotron-120b": { label: "Nemotron 120B", color: "yellow" },
11
+ "nemotron-nano": { label: "Nemotron Nano", color: "yellow" },
12
+ };
13
+ function Badge({ model }) {
14
+ const b = MODEL_BADGE[model] ?? { label: model, color: "gray" };
15
+ return (_jsxs(Text, { color: b.color, bold: true, children: ["[", b.label, "]"] }));
10
16
  }
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..." }))] })] }));
17
+ function ThinkingIndicator({ model }) {
18
+ const b = MODEL_BADGE[model] ?? { label: model, color: "gray" };
19
+ return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: b.color, children: "\u25CF " }), _jsx(Text, { color: b.color, bold: true, children: b.label }), _jsx(Text, { dimColor: true, children: " est\u00E1 pensando..." })] }));
13
20
  }
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)))] }));
21
+ function MessageItem({ msg }) {
22
+ if (msg.role === "user") {
23
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: "white", bold: true, children: "Voc\u00EA" }), _jsx(Text, { children: msg.content })] }));
24
+ }
25
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Badge, { model: msg.model }), msg.elapsed !== undefined && (_jsxs(Text, { dimColor: true, children: [msg.elapsed.toFixed(1), "s"] })), msg.model !== "sonnet" && _jsx(Text, { color: "green", dimColor: true, children: "$0.00" })] }), _jsx(Text, { children: msg.content })] }));
17
26
  }
18
- function StatsBar({ stats }) {
27
+ function StatsBar({ stats, activeProfile }) {
19
28
  const pct = stats.costSaved + stats.costPaid > 0
20
29
  ? Math.round((stats.costSaved / (stats.costSaved + stats.costPaid)) * 100)
21
30
  : 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]" })] }) }));
31
+ return (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { dimColor: true, children: ["perfil: ", _jsx(Text, { color: "white", children: activeProfile }), " ", "tasks: ", _jsx(Text, { color: "white", children: stats.tasksTotal }), " ", "free: ", _jsx(Text, { color: "green", children: stats.tasksFree }), " ", "economizado: ", _jsxs(Text, { color: "green", children: [pct, "%"] }), " ", "gasto: ", _jsxs(Text, { color: "yellow", children: ["$", stats.costPaid.toFixed(4)] })] }), _jsx(Text, { dimColor: true, children: "ctrl+c sair ctrl+l limpar" })] }));
23
32
  }
24
- // ── App principal ─────────────────────────────────────────────────────────────
33
+ // ── App ──────────────────────────────────────────────────────────────────────
25
34
  export function App() {
26
35
  const { exit } = useApp();
27
36
  const [messages, setMessages] = useState([]);
28
- const [routerLog, setRouterLog] = useState(["iniciado", "claude: sonnet-4-6", "free: nemotron-120b"]);
29
37
  const [input, setInput] = useState("");
30
- const [loadingLeft, setLoadingLeft] = useState(false);
31
- const [loadingRight, setLoadingRight] = useState(false);
38
+ const [thinking, setThinking] = useState(null);
32
39
  const [stats, setStats] = useState({
33
40
  tasksTotal: 0, tasksFree: 0, tasksPaid: 0, costPaid: 0, costSaved: 0,
34
41
  });
35
- const addRouter = useCallback((line) => {
36
- setRouterLog((prev) => [...prev, line]);
37
- }, []);
42
+ const profile = loadProfile();
43
+ const activeProfile = profile?.label ?? profile?.name ?? "sem perfil";
38
44
  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})`);
45
+ const { model, reason } = classifyTask(task);
43
46
  const userMsg = {
44
- id: `u-${Date.now()}`,
45
- role: "user",
46
- content: task,
47
- model: decision.model,
48
- timestamp: new Date(),
47
+ id: `u-${Date.now()}`, role: "user",
48
+ content: task, model, timestamp: new Date(),
49
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
- }
50
+ setMessages(prev => [...prev, userMsg]);
51
+ setThinking(model);
52
+ try {
53
+ const result = model === "sonnet"
54
+ ? await runClaude(task)
55
+ : await runOpencode(task, model);
56
+ const assistantMsg = {
57
+ id: `a-${Date.now()}`, role: "assistant",
58
+ content: result.content, model: result.model,
59
+ elapsed: result.elapsed, timestamp: new Date(),
60
+ };
61
+ setMessages(prev => [...prev, assistantMsg]);
62
+ const isFree = model !== "sonnet";
63
+ const cost = isFree ? 0 : (300 / 1000) * COST_PER_1K["sonnet"];
64
+ const saved = isFree ? (300 / 1000) * COST_PER_1K["sonnet"] : 0;
65
+ setStats(s => ({
66
+ tasksTotal: s.tasksTotal + 1,
67
+ tasksFree: s.tasksFree + (isFree ? 1 : 0),
68
+ tasksPaid: s.tasksPaid + (isFree ? 0 : 1),
69
+ costPaid: s.costPaid + cost,
70
+ costSaved: s.costSaved + saved,
71
+ }));
71
72
  }
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
- }
73
+ finally {
74
+ setThinking(null);
91
75
  }
92
- }, [addRouter]);
76
+ }, []);
93
77
  useInput((char, key) => {
94
78
  if (key.ctrl && char === "c") {
95
79
  exit();
@@ -97,25 +81,24 @@ export function App() {
97
81
  }
98
82
  if (key.ctrl && char === "l") {
99
83
  setMessages([]);
100
- setRouterLog(["limpo"]);
101
84
  return;
102
85
  }
103
86
  if (key.return) {
104
87
  const task = input.trim();
105
- if (task) {
88
+ if (task && !thinking) {
106
89
  setInput("");
107
90
  dispatch(task);
108
91
  }
109
92
  return;
110
93
  }
111
94
  if (key.backspace || key.delete) {
112
- setInput((prev) => prev.slice(0, -1));
95
+ setInput(p => p.slice(0, -1));
113
96
  return;
114
97
  }
115
98
  if (char && !key.ctrl && !key.meta) {
116
- setInput((prev) => prev + char);
99
+ setInput(p => p + char);
117
100
  }
118
101
  });
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 })] }));
102
+ return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "arsenal-agent " }), _jsx(Text, { dimColor: true, children: "Claude Sonnet + Nemotron free \u2014 roteamento autom\u00E1tico" })] }), _jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, minHeight: 20, children: [messages.length === 0 && (_jsx(Text, { dimColor: true, children: "Digite uma task e pressione Enter." })), messages.map(msg => _jsx(MessageItem, { msg: msg }, msg.id)), thinking && _jsx(ThinkingIndicator, { model: thinking })] }), _jsxs(Box, { borderStyle: "single", borderColor: thinking ? "gray" : "white", paddingX: 1, children: [_jsxs(Text, { color: thinking ? "gray" : "white", children: [">", " "] }), _jsx(Text, { children: input }), !thinking && _jsx(Text, { color: "white", children: "\u2588" })] }), _jsx(StatsBar, { stats: stats, activeProfile: activeProfile })] }));
120
103
  }
121
104
  //# sourceMappingURL=app.js.map
package/dist/index.js CHANGED
@@ -1,57 +1,61 @@
1
1
  #!/usr/bin/env node
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import { render } from "ink";
2
+ import { spawn } from "node:child_process";
3
+ import { createInterface } from "node:readline";
4
4
  import { Command } from "commander";
5
- import { App } from "./app.js";
6
- import { addProfile, listProfiles, useProfile, loadProfile, applyProfile } from "./profiles.js";
5
+ import chalk from "chalk";
6
+ import { classifyTask } from "./router.js";
7
+ import { loadProfile, applyProfile, addProfile, listProfiles, useProfile } from "./profiles.js";
7
8
  const program = new Command();
8
9
  program
9
10
  .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
11
+ .description("Router: classifica a task e lança claude ou opencode automaticamente")
12
+ .version("0.1.2")
13
+ .option("-p, --profile <name>", "perfil a usar")
14
+ .argument("[task...]", "task a executar diretamente");
15
+ const profileCmd = program.command("profile").description("gerenciar perfis");
16
+ profileCmd.command("add [name]").description("adicionar perfil").action(async (name) => { await addProfile(name); process.exit(0); });
17
+ profileCmd.command("list").alias("ls").description("listar perfis").action(() => { listProfiles(); process.exit(0); });
18
+ profileCmd.command("use <name>").description("definir perfil ativo").action((name) => { useProfile(name); process.exit(0); });
19
+ program.action(async (taskArgs, opts) => {
20
+ // Aplica perfil
42
21
  const profile = loadProfile(opts.profile);
43
22
  if (profile) {
44
23
  applyProfile(profile);
45
- process.stderr.write(`[arsenal-agent] perfil: ${profile.label ?? profile.name}\n`);
24
+ process.stderr.write(chalk.dim(`[${profile.label ?? profile.name}]\n`));
46
25
  }
47
26
  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-...");
27
+ console.error(chalk.red("Nenhuma conta configurada. Rode: aa profile add"));
51
28
  process.exit(1);
52
29
  }
53
- const { waitUntilExit } = render(_jsx(App, {}));
54
- waitUntilExit().then(() => process.exit(0));
30
+ // Task via argumento ou prompt interativo
31
+ const task = taskArgs.length > 0
32
+ ? taskArgs.join(" ")
33
+ : await askTask();
34
+ if (!task.trim())
35
+ process.exit(0);
36
+ // Classifica e lança
37
+ const { model, reason } = classifyTask(task);
38
+ const isFree = model !== "sonnet";
39
+ console.log(chalk.dim("→ ") +
40
+ (isFree ? chalk.yellow("Nemotron 120B") : chalk.cyan("Claude Sonnet")) +
41
+ chalk.dim(` (${reason})`) +
42
+ (isFree ? chalk.green(" free") : ""));
43
+ console.log();
44
+ if (model === "sonnet") {
45
+ launch("claude", [task]);
46
+ }
47
+ else {
48
+ launch("opencode", ["run", task, "--model", "openrouter/nvidia/nemotron-3-super-120b-a12b:free"]);
49
+ }
55
50
  });
51
+ function askTask() {
52
+ return new Promise((resolve) => {
53
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
54
+ rl.question(chalk.white("> "), (answer) => { rl.close(); resolve(answer.trim()); });
55
+ });
56
+ }
57
+ function launch(cmd, args) {
58
+ const child = spawn(cmd, args, { stdio: "inherit", env: process.env });
59
+ child.on("exit", (code) => process.exit(code ?? 0));
60
+ }
56
61
  program.parse();
57
- //# sourceMappingURL=index.js.map
package/dist/profiles.js CHANGED
@@ -168,4 +168,3 @@ export function applyProfile(profile) {
168
168
  process.env.OPENROUTER_API_KEY = profile.openrouter_api_key;
169
169
  }
170
170
  }
171
- //# sourceMappingURL=profiles.js.map
package/dist/router.js CHANGED
@@ -1,17 +1,3 @@
1
- const MODELS = {
2
- "nemotron-120b": "openrouter/nvidia/nemotron-3-super-120b-a12b:free",
3
- "nemotron-nano": "openrouter/nvidia/nemotron-nano-12b-v2-vl:free",
4
- "sonnet": "anthropic/claude-sonnet-4-6",
5
- };
6
- // Custo por 1k tokens (output estimado ~300 tokens)
7
- export const COST_PER_1K = {
8
- "nemotron-120b": 0.0,
9
- "nemotron-nano": 0.0,
10
- "sonnet": 0.003,
11
- };
12
- export function getModelId(alias) {
13
- return MODELS[alias];
14
- }
15
1
  const COMPLEX_PATTERNS = [
16
2
  /arquitetura|architecture/i,
17
3
  /seguran[cç]a|security|vulnerabilidade/i,
@@ -39,4 +25,3 @@ export function classifyTask(task) {
39
25
  }
40
26
  return { model: "nemotron-120b", reason: "task simples → free" };
41
27
  }
42
- //# sourceMappingURL=router.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "arsenal-agent",
3
- "version": "0.1.0",
4
- "description": "Dual AI agent CLI routes tasks between Claude Sonnet and Nemotron (free) automatically",
3
+ "version": "0.1.2",
4
+ "description": "Task router: classifies and launches claude or opencode automatically",
5
5
  "license": "MIT",
6
6
  "author": "DBC Tech",
7
7
  "type": "module",
@@ -12,19 +12,13 @@
12
12
  "scripts": {
13
13
  "build": "tsc",
14
14
  "dev": "tsx src/index.tsx",
15
- "start": "node dist/index.js",
16
15
  "prepublishOnly": "npm run build"
17
16
  },
18
17
  "dependencies": {
19
- "@anthropic-ai/sdk": "^0.51.0",
20
- "@opencode-ai/sdk": "^1.3.13",
21
- "ink": "^5.2.1",
22
- "react": "^18.3.1",
23
18
  "chalk": "^5.4.1",
24
19
  "commander": "^13.1.0"
25
20
  },
26
21
  "devDependencies": {
27
- "@types/react": "^18.3.3",
28
22
  "@types/node": "^22.0.0",
29
23
  "typescript": "^5.8.3",
30
24
  "tsx": "^4.19.2"
@@ -32,9 +26,9 @@
32
26
  "engines": {
33
27
  "node": ">=20"
34
28
  },
35
- "keywords": ["ai", "claude", "opencode", "nemotron", "cli", "agent", "dual-agent"],
29
+ "keywords": ["ai", "claude", "opencode", "nemotron", "cli", "router"],
36
30
  "repository": {
37
31
  "type": "git",
38
- "url": "https://github.com/dbc-tech/arsenal-agent.git"
32
+ "url": "git+https://github.com/dbezerra95/arsenal-cli.git"
39
33
  }
40
34
  }
package/src/index.tsx CHANGED
@@ -1,64 +1,72 @@
1
1
  #!/usr/bin/env node
2
- import React from "react"
3
- import { render } from "ink"
2
+ import { spawn } from "node:child_process"
3
+ import { createInterface } from "node:readline"
4
4
  import { Command } from "commander"
5
- import { App } from "./app.js"
6
- import { addProfile, listProfiles, useProfile, loadProfile, applyProfile } from "./profiles.js"
5
+ import chalk from "chalk"
6
+ import { classifyTask } from "./router.js"
7
+ import { loadProfile, applyProfile, addProfile, listProfiles, useProfile } from "./profiles.js"
7
8
 
8
9
  const program = new Command()
9
10
 
10
11
  program
11
12
  .name("arsenal-agent")
12
- .description("Dual AI agent CLI Claude Sonnet + Nemotron free, roteamento automatico")
13
- .version("0.1.0")
14
- .option("-p, --profile <name>", "perfil a usar (substitui env vars)")
13
+ .description("Router: classifica a task e lança claude ou opencode automaticamente")
14
+ .version("0.1.2")
15
+ .option("-p, --profile <name>", "perfil a usar")
16
+ .argument("[task...]", "task a executar diretamente")
15
17
 
16
- // ── Subcomando: profile ────────────────────────────────────────────────────
17
- const profileCmd = program
18
- .command("profile")
19
- .description("gerenciar perfis de API keys")
18
+ const profileCmd = program.command("profile").description("gerenciar perfis")
19
+ profileCmd.command("add [name]").description("adicionar perfil").action(async (name) => { await addProfile(name); process.exit(0) })
20
+ profileCmd.command("list").alias("ls").description("listar perfis").action(() => { listProfiles(); process.exit(0) })
21
+ profileCmd.command("use <name>").description("definir perfil ativo").action((name) => { useProfile(name); process.exit(0) })
20
22
 
21
- profileCmd
22
- .command("add [name]")
23
- .description("adicionar ou atualizar um perfil")
24
- .action(async (name?: string) => {
25
- await addProfile(name)
26
- process.exit(0)
27
- })
28
-
29
- profileCmd
30
- .command("list")
31
- .alias("ls")
32
- .description("listar perfis cadastrados")
33
- .action(() => {
34
- listProfiles()
35
- process.exit(0)
36
- })
37
-
38
- profileCmd
39
- .command("use <name>")
40
- .description("definir perfil ativo padrão")
41
- .action((name: string) => {
42
- useProfile(name)
43
- process.exit(0)
44
- })
45
-
46
- // ── Ação principal: lança TUI ──────────────────────────────────────────────
47
- program.action((opts: { profile?: string }) => {
48
- // Aplica perfil se informado ou se há um perfil ativo salvo
23
+ program.action(async (taskArgs: string[], opts: { profile?: string }) => {
24
+ // Aplica perfil
49
25
  const profile = loadProfile(opts.profile)
50
26
  if (profile) {
51
27
  applyProfile(profile)
52
- process.stderr.write(`[arsenal-agent] perfil: ${profile.label ?? profile.name}\n`)
28
+ process.stderr.write(chalk.dim(`[${profile.label ?? profile.name}]\n`))
53
29
  } else if (!process.env.ANTHROPIC_API_KEY) {
54
- console.error("Nenhuma ANTHROPIC_API_KEY encontrada.")
55
- console.error("Adicione um perfil: aa profile add")
56
- console.error("Ou exporte a variável: export ANTHROPIC_API_KEY=sk-ant-...")
30
+ console.error(chalk.red("Nenhuma conta configurada. Rode: aa profile add"))
57
31
  process.exit(1)
58
32
  }
59
33
 
60
- const { waitUntilExit } = render(<App />)
61
- waitUntilExit().then(() => process.exit(0))
34
+ // Task via argumento ou prompt interativo
35
+ const task = taskArgs.length > 0
36
+ ? taskArgs.join(" ")
37
+ : await askTask()
38
+
39
+ if (!task.trim()) process.exit(0)
40
+
41
+ // Classifica e lança
42
+ const { model, reason } = classifyTask(task)
43
+ const isFree = model !== "sonnet"
44
+
45
+ console.log(
46
+ chalk.dim("→ ") +
47
+ (isFree ? chalk.yellow("Nemotron 120B") : chalk.cyan("Claude Sonnet")) +
48
+ chalk.dim(` (${reason})`) +
49
+ (isFree ? chalk.green(" free") : "")
50
+ )
51
+ console.log()
52
+
53
+ if (model === "sonnet") {
54
+ launch("claude", [task])
55
+ } else {
56
+ launch("opencode", ["run", task, "--model", "openrouter/nvidia/nemotron-3-super-120b-a12b:free"])
57
+ }
62
58
  })
63
59
 
60
+ function askTask(): Promise<string> {
61
+ return new Promise((resolve) => {
62
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
63
+ rl.question(chalk.white("> "), (answer) => { rl.close(); resolve(answer.trim()) })
64
+ })
65
+ }
66
+
67
+ function launch(cmd: string, args: string[]): void {
68
+ const child = spawn(cmd, args, { stdio: "inherit", env: process.env })
69
+ child.on("exit", (code) => process.exit(code ?? 0))
70
+ }
71
+
64
72
  program.parse()
package/src/router.ts CHANGED
@@ -1,20 +1,8 @@
1
- import type { ModelAlias, RouterDecision } from "./types.js"
1
+ export type ModelAlias = "nemotron-120b" | "nemotron-nano" | "sonnet"
2
2
 
3
- const MODELS: Record<ModelAlias, string> = {
4
- "nemotron-120b": "openrouter/nvidia/nemotron-3-super-120b-a12b:free",
5
- "nemotron-nano": "openrouter/nvidia/nemotron-nano-12b-v2-vl:free",
6
- "sonnet": "anthropic/claude-sonnet-4-6",
7
- }
8
-
9
- // Custo por 1k tokens (output estimado ~300 tokens)
10
- export const COST_PER_1K: Record<ModelAlias, number> = {
11
- "nemotron-120b": 0.0,
12
- "nemotron-nano": 0.0,
13
- "sonnet": 0.003,
14
- }
15
-
16
- export function getModelId(alias: ModelAlias): string {
17
- return MODELS[alias]
3
+ export interface RouterDecision {
4
+ model: ModelAlias
5
+ reason: string
18
6
  }
19
7
 
20
8
  const COMPLEX_PATTERNS = [
package/tsconfig.json CHANGED
@@ -3,15 +3,11 @@
3
3
  "target": "ES2022",
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "bundler",
6
- "jsx": "react-jsx",
7
- "jsxImportSource": "react",
8
6
  "outDir": "./dist",
9
7
  "rootDir": "./src",
10
8
  "strict": true,
11
9
  "esModuleInterop": true,
12
- "skipLibCheck": true,
13
- "declaration": true,
14
- "sourceMap": true
10
+ "skipLibCheck": true
15
11
  },
16
12
  "include": ["src/**/*"],
17
13
  "exclude": ["node_modules", "dist"]
package/src/agents.ts DELETED
@@ -1,54 +0,0 @@
1
- import { execFile } from "node:child_process"
2
- import { promisify } from "node:util"
3
- import Anthropic from "@anthropic-ai/sdk"
4
- import type { ModelAlias } from "./types.js"
5
- import { getModelId } from "./router.js"
6
-
7
- const execFileAsync = promisify(execFile)
8
-
9
- const anthropic = new Anthropic({
10
- apiKey: process.env.ANTHROPIC_API_KEY,
11
- })
12
-
13
- export interface AgentResult {
14
- content: string
15
- elapsed: number
16
- model: ModelAlias
17
- }
18
-
19
- export async function runClaude(task: string): Promise<AgentResult> {
20
- const t0 = Date.now()
21
-
22
- const message = await anthropic.messages.create({
23
- model: "claude-sonnet-4-6-20251101",
24
- max_tokens: 2048,
25
- messages: [{ role: "user", content: task }],
26
- })
27
-
28
- const content = message.content
29
- .filter((b) => b.type === "text")
30
- .map((b) => (b as { type: "text"; text: string }).text)
31
- .join("\n")
32
-
33
- return { content, elapsed: (Date.now() - t0) / 1000, model: "sonnet" }
34
- }
35
-
36
- export async function runOpencode(task: string, alias: ModelAlias): Promise<AgentResult> {
37
- const t0 = Date.now()
38
- const modelId = getModelId(alias)
39
-
40
- try {
41
- const { stdout, stderr } = await execFileAsync(
42
- "opencode",
43
- ["run", task, "--model", modelId],
44
- { timeout: 120_000, env: process.env }
45
- )
46
- const raw = stdout || stderr || "Sem resposta."
47
- // Remove ANSI escape codes
48
- const content = raw.replace(/\x1b\[[0-9;]*m/g, "").replace(/\[[\d]+m/g, "").trim()
49
- return { content, elapsed: (Date.now() - t0) / 1000, model: alias }
50
- } catch (err: unknown) {
51
- const msg = err instanceof Error ? err.message : String(err)
52
- return { content: `Erro: ${msg}`, elapsed: (Date.now() - t0) / 1000, model: alias }
53
- }
54
- }
package/src/app.tsx DELETED
@@ -1,236 +0,0 @@
1
- import React, { useState, useCallback } from "react"
2
- import { Box, Text, useInput, useApp } from "ink"
3
- import type { Message, Stats, RouterDecision } from "./types.js"
4
- import { classifyTask, COST_PER_1K } from "./router.js"
5
- import { runClaude, runOpencode } from "./agents.js"
6
-
7
- // ── Componentes ──────────────────────────────────────────────────────────────
8
-
9
- function MessageList({ messages, model }: { messages: Message[]; model: "sonnet" | "nemotron" }) {
10
- const filtered = messages.filter((m) =>
11
- model === "sonnet" ? m.model === "sonnet" : m.model !== "sonnet"
12
- )
13
-
14
- return (
15
- <Box flexDirection="column" gap={0}>
16
- {filtered.map((msg) => (
17
- <Box key={msg.id} flexDirection="column" marginBottom={1}>
18
- <Text color={msg.role === "user" ? "cyan" : "green"} bold>
19
- {msg.role === "user" ? "Você" : model === "sonnet" ? "Claude" : "Nemotron"}
20
- {msg.elapsed ? ` (${msg.elapsed.toFixed(1)}s)` : ""}
21
- </Text>
22
- <Text wrap="wrap">{msg.content}</Text>
23
- </Box>
24
- ))}
25
- </Box>
26
- )
27
- }
28
-
29
- function Panel({
30
- title,
31
- color,
32
- messages,
33
- model,
34
- loading,
35
- width,
36
- }: {
37
- title: string
38
- color: string
39
- messages: Message[]
40
- model: "sonnet" | "nemotron"
41
- loading: boolean
42
- width: number
43
- }) {
44
- return (
45
- <Box
46
- flexDirection="column"
47
- borderStyle="round"
48
- borderColor={color as "cyan" | "yellow"}
49
- width={width}
50
- height={30}
51
- paddingX={1}
52
- >
53
- <Text bold color={color as "cyan" | "yellow"}>
54
- {title}
55
- </Text>
56
- <Box flexDirection="column" flexGrow={1} overflow="hidden">
57
- <MessageList messages={messages} model={model} />
58
- {loading && (
59
- <Text color="gray" dimColor>
60
- aguardando...
61
- </Text>
62
- )}
63
- </Box>
64
- </Box>
65
- )
66
- }
67
-
68
- function RouterLog({ entries }: { entries: string[] }) {
69
- const last = entries.slice(-12)
70
- return (
71
- <Box
72
- flexDirection="column"
73
- borderStyle="round"
74
- borderColor="gray"
75
- width={26}
76
- height={30}
77
- paddingX={1}
78
- >
79
- <Text bold color="gray">
80
- ROUTER
81
- </Text>
82
- {last.map((e, i) => (
83
- <Text key={i} color="gray" dimColor wrap="wrap">
84
- {e}
85
- </Text>
86
- ))}
87
- </Box>
88
- )
89
- }
90
-
91
- function StatsBar({ stats }: { stats: Stats }) {
92
- const pct =
93
- stats.costSaved + stats.costPaid > 0
94
- ? Math.round((stats.costSaved / (stats.costSaved + stats.costPaid)) * 100)
95
- : 0
96
-
97
- return (
98
- <Box borderStyle="single" borderColor="gray" paddingX={2} marginTop={0}>
99
- <Text>
100
- Tasks: <Text bold>{stats.tasksTotal}</Text>{" "}
101
- Free: <Text color="green" bold>{stats.tasksFree}</Text>{" "}
102
- Pago: <Text color="yellow" bold>{stats.tasksPaid}</Text>{" "}
103
- Economizado: <Text color="green" bold>{pct}%</Text> (~${stats.costSaved.toFixed(4)}){" "}
104
- Gasto: <Text color="yellow">${stats.costPaid.toFixed(4)}</Text>{" "}
105
- <Text dimColor>[ctrl+c sair • ctrl+l limpar]</Text>
106
- </Text>
107
- </Box>
108
- )
109
- }
110
-
111
- // ── App principal ─────────────────────────────────────────────────────────────
112
-
113
- export function App() {
114
- const { exit } = useApp()
115
- const [messages, setMessages] = useState<Message[]>([])
116
- const [routerLog, setRouterLog] = useState<string[]>(["iniciado", "claude: sonnet-4-6", "free: nemotron-120b"])
117
- const [input, setInput] = useState("")
118
- const [loadingLeft, setLoadingLeft] = useState(false)
119
- const [loadingRight, setLoadingRight] = useState(false)
120
- const [stats, setStats] = useState<Stats>({
121
- tasksTotal: 0, tasksFree: 0, tasksPaid: 0, costPaid: 0, costSaved: 0,
122
- })
123
-
124
- const addRouter = useCallback((line: string) => {
125
- setRouterLog((prev) => [...prev, line])
126
- }, [])
127
-
128
- const dispatch = useCallback(
129
- async (task: string) => {
130
- const decision: RouterDecision = classifyTask(task)
131
- const ts = new Date().toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit", second: "2-digit" })
132
- addRouter(`${ts} → ${task.slice(0, 22)}...`)
133
- addRouter(` ${decision.model} (${decision.reason})`)
134
-
135
- const userMsg: Message = {
136
- id: `u-${Date.now()}`,
137
- role: "user",
138
- content: task,
139
- model: decision.model,
140
- timestamp: new Date(),
141
- }
142
- setMessages((prev) => [...prev, userMsg])
143
-
144
- if (decision.model === "sonnet") {
145
- setLoadingLeft(true)
146
- try {
147
- const result = await runClaude(task)
148
- const assistantMsg: Message = {
149
- id: `a-${Date.now()}`,
150
- role: "assistant",
151
- content: result.content,
152
- model: "sonnet",
153
- elapsed: result.elapsed,
154
- timestamp: new Date(),
155
- }
156
- setMessages((prev) => [...prev, assistantMsg])
157
- const cost = (300 / 1000) * COST_PER_1K["sonnet"]
158
- const saved = (300 / 1000) * COST_PER_1K["nemotron-120b"]
159
- setStats((s) => ({ ...s, tasksTotal: s.tasksTotal + 1, tasksPaid: s.tasksPaid + 1, costPaid: s.costPaid + cost, costSaved: s.costSaved + saved }))
160
- } finally {
161
- setLoadingLeft(false)
162
- }
163
- } else {
164
- setLoadingRight(true)
165
- try {
166
- const result = await runOpencode(task, decision.model)
167
- const assistantMsg: Message = {
168
- id: `a-${Date.now()}`,
169
- role: "assistant",
170
- content: result.content,
171
- model: decision.model,
172
- elapsed: result.elapsed,
173
- timestamp: new Date(),
174
- }
175
- setMessages((prev) => [...prev, assistantMsg])
176
- const saved = (300 / 1000) * COST_PER_1K["sonnet"]
177
- setStats((s) => ({ ...s, tasksTotal: s.tasksTotal + 1, tasksFree: s.tasksFree + 1, costSaved: s.costSaved + saved }))
178
- } finally {
179
- setLoadingRight(false)
180
- }
181
- }
182
- },
183
- [addRouter]
184
- )
185
-
186
- useInput((char, key) => {
187
- if (key.ctrl && char === "c") { exit(); return }
188
- if (key.ctrl && char === "l") { setMessages([]); setRouterLog(["limpo"]); return }
189
- if (key.return) {
190
- const task = input.trim()
191
- if (task) { setInput(""); dispatch(task) }
192
- return
193
- }
194
- if (key.backspace || key.delete) {
195
- setInput((prev) => prev.slice(0, -1))
196
- return
197
- }
198
- if (char && !key.ctrl && !key.meta) {
199
- setInput((prev) => prev + char)
200
- }
201
- })
202
-
203
- return (
204
- <Box flexDirection="column">
205
- <Box>
206
- <Text bold color="blue"> arsenal-agent </Text>
207
- <Text dimColor>Claude Sonnet + Nemotron free — roteamento automatico</Text>
208
- </Box>
209
- <Box flexDirection="row" gap={0}>
210
- <Panel
211
- title="Claude Sonnet 4.6 (tasks complexas)"
212
- color="cyan"
213
- messages={messages}
214
- model="sonnet"
215
- loading={loadingLeft}
216
- width={50}
217
- />
218
- <RouterLog entries={routerLog} />
219
- <Panel
220
- title="Nemotron 120B (free · tasks simples)"
221
- color="yellow"
222
- messages={messages}
223
- model="nemotron"
224
- loading={loadingRight}
225
- width={50}
226
- />
227
- </Box>
228
- <Box borderStyle="single" borderColor="gray" paddingX={1}>
229
- <Text color="gray">{">"} </Text>
230
- <Text>{input}</Text>
231
- <Text color="gray">█</Text>
232
- </Box>
233
- <StatsBar stats={stats} />
234
- </Box>
235
- )
236
- }
package/src/types.ts DELETED
@@ -1,23 +0,0 @@
1
- export type ModelAlias = "nemotron-120b" | "nemotron-nano" | "sonnet"
2
-
3
- export interface Message {
4
- id: string
5
- role: "user" | "assistant"
6
- content: string
7
- model: ModelAlias
8
- elapsed?: number
9
- timestamp: Date
10
- }
11
-
12
- export interface RouterDecision {
13
- model: ModelAlias
14
- reason: string
15
- }
16
-
17
- export interface Stats {
18
- tasksTotal: number
19
- tasksFree: number
20
- tasksPaid: number
21
- costPaid: number
22
- costSaved: number
23
- }