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 +56 -73
- package/dist/index.js +47 -43
- package/dist/profiles.js +0 -1
- package/dist/router.js +0 -15
- package/package.json +4 -10
- package/src/index.tsx +53 -45
- package/src/router.ts +4 -16
- package/tsconfig.json +1 -5
- package/src/agents.ts +0 -54
- package/src/app.tsx +0 -236
- package/src/types.ts +0 -23
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
12
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
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 (
|
|
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
|
|
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 [
|
|
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
|
|
36
|
-
|
|
37
|
-
}, []);
|
|
42
|
+
const profile = loadProfile();
|
|
43
|
+
const activeProfile = profile?.label ?? profile?.name ?? "sem perfil";
|
|
38
44
|
const dispatch = useCallback(async (task) => {
|
|
39
|
-
const
|
|
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
|
-
|
|
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(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
}, [
|
|
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(
|
|
95
|
+
setInput(p => p.slice(0, -1));
|
|
113
96
|
return;
|
|
114
97
|
}
|
|
115
98
|
if (char && !key.ctrl && !key.meta) {
|
|
116
|
-
setInput(
|
|
99
|
+
setInput(p => p + char);
|
|
117
100
|
}
|
|
118
101
|
});
|
|
119
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "
|
|
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 {
|
|
3
|
-
import {
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
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("
|
|
11
|
-
.version("0.1.
|
|
12
|
-
.option("-p, --profile <name>", "perfil a usar
|
|
13
|
-
|
|
14
|
-
const profileCmd = program
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
profileCmd
|
|
18
|
-
|
|
19
|
-
|
|
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(`[
|
|
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
|
|
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
|
-
|
|
54
|
-
|
|
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
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.
|
|
4
|
-
"description": "
|
|
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", "
|
|
29
|
+
"keywords": ["ai", "claude", "opencode", "nemotron", "cli", "router"],
|
|
36
30
|
"repository": {
|
|
37
31
|
"type": "git",
|
|
38
|
-
"url": "https://github.com/
|
|
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
|
|
3
|
-
import {
|
|
2
|
+
import { spawn } from "node:child_process"
|
|
3
|
+
import { createInterface } from "node:readline"
|
|
4
4
|
import { Command } from "commander"
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
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("
|
|
13
|
-
.version("0.1.
|
|
14
|
-
.option("-p, --profile <name>", "perfil a usar
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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(`[
|
|
28
|
+
process.stderr.write(chalk.dim(`[${profile.label ?? profile.name}]\n`))
|
|
53
29
|
} else if (!process.env.ANTHROPIC_API_KEY) {
|
|
54
|
-
console.error("Nenhuma
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
1
|
+
export type ModelAlias = "nemotron-120b" | "nemotron-nano" | "sonnet"
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
}
|