iaoxe-cli 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/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # iaoxe CLI
2
+
3
+ Orquestación de coding de IAOXE en tu terminal y tu CI. Mismo motor que la app de
4
+ escritorio: **GLM-5.2 escribe el volumen, Claude revisa lo crítico**, con control
5
+ de costo. Sin dependencias (Node ≥ 18).
6
+
7
+ ## Instalación
8
+
9
+ ```bash
10
+ npm install -g iaoxe-cli # paquete: iaoxe-cli · comando: iaoxe
11
+ iaoxe help
12
+ ```
13
+
14
+ Desde el repo (desarrollo):
15
+
16
+ ```bash
17
+ cd iaoxe-cli
18
+ npm link # expone el comando `iaoxe`
19
+ # o directo: node bin/iaoxe.js <comando>
20
+ ```
21
+
22
+ ## Dos modos
23
+
24
+ ### Gestionado (créditos IAOXE) — recomendado
25
+
26
+ ```bash
27
+ iaoxe login # abre Google en el navegador, guarda la sesión en ~/.iaoxe/session.json
28
+ iaoxe whoami # muestra tu cuenta y créditos disponibles
29
+ iaoxe logout
30
+ ```
31
+
32
+ Con sesión, los comandos usan los **créditos IAOXE** (sin gestionar claves): el CLI
33
+ rutea al proxy gestionado, igual que la app de escritorio.
34
+
35
+ ### BYOK (tus propias claves)
36
+
37
+ Exporta las claves de proveedor, o ponlas en `~/.iaoxe/config.json`, y usa `--byok`:
38
+
39
+ ```bash
40
+ export ZAI_API_KEY=... # GLM-5.2 (workhorse)
41
+ export ANTHROPIC_API_KEY=... # Claude (review)
42
+ iaoxe run --byok "..."
43
+ ```
44
+
45
+ ```json
46
+ { "keys": { "ZAI_API_KEY": "...", "ANTHROPIC_API_KEY": "..." } }
47
+ ```
48
+
49
+ > Sin sesión y sin `--byok`, el CLI usa BYOK por defecto. Con sesión, usa créditos
50
+ > IAOXE salvo que pases `--byok`.
51
+
52
+ ## Comandos
53
+
54
+ ```bash
55
+ iaoxe run "add a subtract() to math.js and document it" # AGENTE: lee y edita archivos del repo
56
+ iaoxe run "..." --dry-run # muestra qué editaría, sin escribir
57
+ iaoxe run "..." --print # solo imprime la respuesta (no edita)
58
+ iaoxe review # revisión crítica del diff (vs HEAD)
59
+ iaoxe review --staged # revisa lo que está en stage
60
+ iaoxe review --model claude-opus # usa otro modelo de revisión
61
+ iaoxe models # lista los modelos
62
+ iaoxe help
63
+ ```
64
+
65
+ `run` es un **agente**: lee archivos del repo y los edita (confinado al cwd). Trabaja
66
+ mejor dentro de un repo git para revisar el diff (`git diff` / `iaoxe review`) y revertir
67
+ si hace falta.
68
+
69
+ ### Revisión en CI
70
+
71
+ `iaoxe review` devuelve **exit code 2** si el veredicto es *block* y **1** si pide
72
+ cambios, así que puedes fallar un pipeline ante hallazgos críticos:
73
+
74
+ ```yaml
75
+ - run: iaoxe review --staged
76
+ ```
77
+
78
+ ## Publicar (mantenedores)
79
+
80
+ ```bash
81
+ npm login
82
+ npm publish --access public # paquete: iaoxe-cli (npm bloquea "iaoxe" por similitud)
83
+ ```
84
+
85
+ El paquete incluye `lib/auth-config.js` con la config **client-public** de IAOXE
86
+ (Firebase API key + OAuth client de tipo Desktop), igual que la app de escritorio.
87
+
88
+ ## Estado
89
+
90
+ v1: `login`/`whoami` (créditos IAOXE), **`run` agéntico** (lee/edita archivos) y
91
+ `review` (auto-review headless), en modo gestionado o BYOK.
package/bin/iaoxe.js ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { loadEnv } = require("../lib/config");
5
+ const { routeAliases } = require("../lib/router");
6
+ const { review } = require("../lib/review");
7
+ const { run } = require("../lib/run");
8
+ const { login, clearSession, loadSession } = require("../lib/auth");
9
+ const { billingState } = require("../lib/managed");
10
+ const c = require("../lib/colors");
11
+
12
+ const VERSION = require("../package.json").version;
13
+
14
+ function parseFlags(args) {
15
+ const flags = {};
16
+ const rest = [];
17
+ for (let i = 0; i < args.length; i += 1) {
18
+ const a = args[i];
19
+ if (a === "--staged" || a === "-s") flags.staged = true;
20
+ else if (a === "--byok") flags.byok = true;
21
+ else if (a === "--print") flags.print = true;
22
+ else if (a === "--dry-run") flags.dryRun = true;
23
+ else if (a === "--model" || a === "-m") flags.model = args[++i];
24
+ else if (a.startsWith("--model=")) flags.model = a.slice(8);
25
+ else if (a === "--help" || a === "-h") flags.help = true;
26
+ else rest.push(a);
27
+ }
28
+ return { flags, rest };
29
+ }
30
+
31
+ function help() {
32
+ console.log(`${c.bold("iaoxe")} ${c.dim("v" + VERSION)} — orquestacion de coding en tu terminal
33
+
34
+ ${c.bold("Uso:")}
35
+ iaoxe login Inicia sesion con Google (creditos IAOXE)
36
+ iaoxe whoami Muestra tu cuenta y creditos disponibles
37
+ iaoxe logout Cierra la sesion local
38
+ iaoxe run "<tarea>" [--dry-run|--print] Agente: lee y edita archivos del repo
39
+ iaoxe review [--staged] [--model <a>] Revision critica del diff (auth/pagos/seguridad)
40
+ iaoxe models Lista los modelos disponibles
41
+ iaoxe help | --version
42
+
43
+ ${c.bold("Modos:")} con sesion usa creditos IAOXE; --byok fuerza tus propias claves.
44
+ ${c.bold("Claves (BYOK):")} exporta ZAI_API_KEY / ANTHROPIC_API_KEY, o ~/.iaoxe/config.json
45
+ ${c.dim("review devuelve exit code 2 si bloquea y 1 si pide cambios (util en CI).")}`);
46
+ }
47
+
48
+ async function main() {
49
+ const argv = process.argv.slice(2);
50
+ const command = argv[0];
51
+ const { flags, rest } = parseFlags(argv.slice(1));
52
+ const env = loadEnv();
53
+
54
+ if (!command || command === "help" || flags.help) {
55
+ help();
56
+ return 0;
57
+ }
58
+ if (command === "--version" || command === "-v" || command === "version") {
59
+ console.log(VERSION);
60
+ return 0;
61
+ }
62
+ if (command === "models") {
63
+ console.log(routeAliases().join("\n"));
64
+ return 0;
65
+ }
66
+ if (command === "login") {
67
+ const session = await login();
68
+ console.log(c.green(`Sesion iniciada como ${session.email || session.uid}.`));
69
+ return 0;
70
+ }
71
+ if (command === "logout") {
72
+ clearSession();
73
+ console.log(c.dim("Sesion local cerrada."));
74
+ return 0;
75
+ }
76
+ if (command === "whoami") {
77
+ const session = loadSession();
78
+ if (!session) {
79
+ console.log(c.dim("Sin sesion. Ejecuta: iaoxe login"));
80
+ return 1;
81
+ }
82
+ console.log(`${c.bold(session.email || session.uid)}`);
83
+ try {
84
+ const billing = await billingState();
85
+ console.log(
86
+ `${c.dim("Plan:")} ${billing.planName || billing.planId} · ${c.green(
87
+ `${Math.round(Number(billing.availableCredits || 0))} creditos`,
88
+ )} ${c.dim(`de ${Math.round(Number(billing.hardLimit || 0))}`)}`,
89
+ );
90
+ } catch (error) {
91
+ console.log(c.dim(`(no se pudo leer billing: ${error.message})`));
92
+ }
93
+ return 0;
94
+ }
95
+ if (command === "run") {
96
+ return run({
97
+ task: rest.join(" "),
98
+ model: flags.model,
99
+ env,
100
+ forceByok: Boolean(flags.byok),
101
+ print: Boolean(flags.print),
102
+ dryRun: Boolean(flags.dryRun),
103
+ });
104
+ }
105
+ if (command === "review") {
106
+ return review({ staged: Boolean(flags.staged), model: flags.model || "claude-review", env, forceByok: Boolean(flags.byok) });
107
+ }
108
+
109
+ console.error(c.amber(`Comando desconocido: ${command}`));
110
+ help();
111
+ return 1;
112
+ }
113
+
114
+ main()
115
+ .then((code) => process.exit(typeof code === "number" ? code : 0))
116
+ .catch((error) => {
117
+ console.error(c.red(`Error: ${error.message}`));
118
+ process.exit(1);
119
+ });
package/lib/agent.js ADDED
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { execFileSync } = require("child_process");
6
+ const { complete, activeMode } = require("./complete");
7
+ const c = require("./colors");
8
+
9
+ const MAX_STEPS = 14;
10
+
11
+ const TOOLS = [
12
+ {
13
+ type: "function",
14
+ function: {
15
+ name: "list_files",
16
+ description: "Lista archivos del repositorio (rutas relativas).",
17
+ parameters: { type: "object", properties: { dir: { type: "string", description: "subdirectorio opcional" } } },
18
+ },
19
+ },
20
+ {
21
+ type: "function",
22
+ function: {
23
+ name: "read_file",
24
+ description: "Lee el contenido de un archivo del repo.",
25
+ parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
26
+ },
27
+ },
28
+ {
29
+ type: "function",
30
+ function: {
31
+ name: "write_file",
32
+ description: "Crea o sobrescribe un archivo del repo con el contenido completo dado.",
33
+ parameters: {
34
+ type: "object",
35
+ properties: { path: { type: "string" }, content: { type: "string" } },
36
+ required: ["path", "content"],
37
+ },
38
+ },
39
+ },
40
+ ];
41
+
42
+ const SYSTEM = [
43
+ "Eres un agente de coding que trabaja en el repositorio del usuario mediante herramientas.",
44
+ "Lee los archivos relevantes antes de editar. Haz los cambios minimos y correctos para cumplir la tarea.",
45
+ "Usa write_file con el contenido COMPLETO del archivo (no fragmentos).",
46
+ "Cuando termines, responde SIN llamar herramientas con un resumen breve de lo que hiciste.",
47
+ ].join(" ");
48
+
49
+ function safeResolve(root, p) {
50
+ const abs = path.resolve(root, String(p || ""));
51
+ const base = path.resolve(root);
52
+ if (abs !== base && !abs.startsWith(base + path.sep)) throw new Error(`ruta fuera del repo: ${p}`);
53
+ return abs;
54
+ }
55
+
56
+ function listFiles(root, dir) {
57
+ try {
58
+ const out = execFileSync("git", ["ls-files"], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 });
59
+ let files = out.split(/\r?\n/).filter(Boolean);
60
+ if (dir) files = files.filter((f) => f.startsWith(String(dir).replace(/\\/g, "/")));
61
+ return files.slice(0, 300).join("\n") || "(sin archivos trackeados)";
62
+ } catch {
63
+ const base = safeResolve(root, dir || ".");
64
+ try {
65
+ return fs.readdirSync(base).slice(0, 200).join("\n");
66
+ } catch (error) {
67
+ return `error: ${error.message}`;
68
+ }
69
+ }
70
+ }
71
+
72
+ async function agentRun({ task, model = "glm-coding", env, forceByok = false, dryRun = false, root = process.cwd() }) {
73
+ const messages = [
74
+ { role: "system", content: SYSTEM },
75
+ { role: "user", content: task },
76
+ ];
77
+ const changed = [];
78
+ process.stderr.write(c.dim(`agente · ${model} · ${activeMode({ forceByok })}${dryRun ? " · dry-run" : ""}\n`));
79
+
80
+ for (let step = 0; step < MAX_STEPS; step += 1) {
81
+ const { message } = await complete({
82
+ alias: model,
83
+ env,
84
+ forceByok,
85
+ stream: false,
86
+ maxTokens: 4000,
87
+ temperature: 0.2,
88
+ messages,
89
+ tools: TOOLS,
90
+ });
91
+ messages.push(message);
92
+ const calls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
93
+
94
+ if (!calls.length) {
95
+ if (message.content) console.log(`\n${message.content}`);
96
+ break;
97
+ }
98
+
99
+ for (const call of calls) {
100
+ const name = call.function?.name;
101
+ let args = {};
102
+ try {
103
+ args = JSON.parse(call.function?.arguments || "{}");
104
+ } catch {
105
+ args = {};
106
+ }
107
+ let result = "";
108
+ try {
109
+ if (name === "list_files") {
110
+ result = listFiles(root, args.dir);
111
+ } else if (name === "read_file") {
112
+ const abs = safeResolve(root, args.path);
113
+ result = fs.readFileSync(abs, "utf8").slice(0, 24000);
114
+ console.log(c.dim(` · read ${args.path}`));
115
+ } else if (name === "write_file") {
116
+ const abs = safeResolve(root, args.path);
117
+ if (dryRun) {
118
+ console.log(c.amber(` · (dry-run) escribiria ${args.path} (${(args.content || "").length} b)`));
119
+ result = "dry-run: no escrito";
120
+ } else {
121
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
122
+ fs.writeFileSync(abs, String(args.content || ""), "utf8");
123
+ if (!changed.includes(args.path)) changed.push(args.path);
124
+ console.log(c.green(` · escribio ${args.path}`));
125
+ result = "ok";
126
+ }
127
+ } else {
128
+ result = `herramienta desconocida: ${name}`;
129
+ }
130
+ } catch (error) {
131
+ result = `error: ${error.message}`;
132
+ }
133
+ messages.push({ role: "tool", tool_call_id: call.id, content: String(result).slice(0, 24000) });
134
+ }
135
+ }
136
+
137
+ if (changed.length) {
138
+ console.log(c.green(`\n${changed.length} archivo(s) ${dryRun ? "a modificar" : "modificado(s)"}: `) + changed.join(", "));
139
+ if (!dryRun) console.log(c.dim("Revisa con: git diff · iaoxe review"));
140
+ } else {
141
+ console.log(c.dim("\nSin cambios de archivos."));
142
+ }
143
+ return changed.length && !dryRun ? 0 : 0;
144
+ }
145
+
146
+ module.exports = { agentRun };
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ // Config client-public de IAOXE para el login gestionado del CLI (mismos valores
4
+ // que el desktop). Permite OAuth Google -> token Firebase -> proxy de inferencia.
5
+ module.exports = {
6
+ FIREBASE_API_KEY: "AIzaSyACQMu0Ky7GTsTbHfpm0WStVMYtsyUVbYU",
7
+ GOOGLE_OAUTH_CLIENT_ID: "506222517298-34f0qj15milisudbkh4t6ulp3gkoq78n.apps.googleusercontent.com",
8
+ GOOGLE_OAUTH_CLIENT_SECRET: "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",
9
+ IAOXE_BACKEND_URL: process.env.IAOXE_BACKEND_URL || "https://us-central1-iaoxe-ai.cloudfunctions.net",
10
+ };
package/lib/auth.js ADDED
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+
3
+ const http = require("http");
4
+ const crypto = require("crypto");
5
+ const fs = require("fs");
6
+ const os = require("os");
7
+ const path = require("path");
8
+ const { exec } = require("child_process");
9
+ const cfg = require("./auth-config");
10
+
11
+ const SESSION_PATH = path.join(os.homedir(), ".iaoxe", "session.json");
12
+
13
+ function base64Url(buf) {
14
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
15
+ }
16
+
17
+ function openBrowser(url) {
18
+ const platform = process.platform;
19
+ const cmd =
20
+ platform === "win32" ? `start "" "${url}"` : platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
21
+ exec(cmd, () => {});
22
+ }
23
+
24
+ function loadSession() {
25
+ try {
26
+ return JSON.parse(fs.readFileSync(SESSION_PATH, "utf8"));
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ function saveSession(session) {
33
+ fs.mkdirSync(path.dirname(SESSION_PATH), { recursive: true });
34
+ fs.writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2), "utf8");
35
+ return session;
36
+ }
37
+
38
+ function clearSession() {
39
+ try {
40
+ fs.unlinkSync(SESSION_PATH);
41
+ } catch {
42
+ // no session
43
+ }
44
+ }
45
+
46
+ function waitForCode(server) {
47
+ return new Promise((resolve, reject) => {
48
+ const timeout = setTimeout(() => {
49
+ server.close();
50
+ reject(new Error("El login expiro antes de recibir respuesta de Google."));
51
+ }, 180000);
52
+ server.on("request", (request, response) => {
53
+ const url = new URL(request.url, "http://127.0.0.1");
54
+ if (url.pathname !== "/callback") {
55
+ response.writeHead(404);
56
+ response.end("Not found");
57
+ return;
58
+ }
59
+ clearTimeout(timeout);
60
+ const error = url.searchParams.get("error");
61
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
62
+ response.end(
63
+ "<html><body style=\"font-family:system-ui;background:#0b0b0c;color:#eee;text-align:center;padding-top:18vh\"><h2>IAOXE</h2><p>Sesion iniciada. Vuelve a tu terminal.</p></body></html>",
64
+ );
65
+ server.close();
66
+ if (error) reject(new Error(`Google rechazo el login: ${error}`));
67
+ else resolve(url.searchParams.get("code"));
68
+ });
69
+ });
70
+ }
71
+
72
+ async function login() {
73
+ const verifier = base64Url(crypto.randomBytes(64));
74
+ const challenge = base64Url(crypto.createHash("sha256").update(verifier).digest());
75
+
76
+ const server = http.createServer();
77
+ await new Promise((resolve, reject) => {
78
+ server.once("error", reject);
79
+ server.listen(0, "127.0.0.1", resolve);
80
+ });
81
+ const port = server.address().port;
82
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
83
+
84
+ const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth");
85
+ authUrl.searchParams.set("client_id", cfg.GOOGLE_OAUTH_CLIENT_ID);
86
+ authUrl.searchParams.set("redirect_uri", redirectUri);
87
+ authUrl.searchParams.set("response_type", "code");
88
+ authUrl.searchParams.set("scope", "openid email profile");
89
+ authUrl.searchParams.set("code_challenge", challenge);
90
+ authUrl.searchParams.set("code_challenge_method", "S256");
91
+ authUrl.searchParams.set("prompt", "select_account");
92
+
93
+ const codePromise = waitForCode(server);
94
+ openBrowser(authUrl.toString());
95
+ process.stderr.write("Abre esta URL si el navegador no se abrio:\n" + authUrl.toString() + "\n");
96
+ const code = await codePromise;
97
+ if (!code) throw new Error("Google no devolvio codigo de autorizacion.");
98
+
99
+ const tokenBody = new URLSearchParams({
100
+ code,
101
+ client_id: cfg.GOOGLE_OAUTH_CLIENT_ID,
102
+ client_secret: cfg.GOOGLE_OAUTH_CLIENT_SECRET,
103
+ code_verifier: verifier,
104
+ redirect_uri: redirectUri,
105
+ grant_type: "authorization_code",
106
+ });
107
+ const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
108
+ method: "POST",
109
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
110
+ body: tokenBody,
111
+ });
112
+ const token = await tokenResponse.json();
113
+ if (!tokenResponse.ok) throw new Error(token.error_description || token.error || "OAuth fallo.");
114
+
115
+ const idp = new URLSearchParams({ providerId: "google.com", id_token: token.id_token || "" });
116
+ const fbResponse = await fetch(
117
+ `https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp?key=${encodeURIComponent(cfg.FIREBASE_API_KEY)}`,
118
+ {
119
+ method: "POST",
120
+ headers: { "Content-Type": "application/json" },
121
+ body: JSON.stringify({ postBody: idp.toString(), requestUri: "http://localhost", returnIdpCredential: true, returnSecureToken: true }),
122
+ },
123
+ );
124
+ const fb = await fbResponse.json();
125
+ if (!fbResponse.ok) throw new Error(fb.error?.message || "Firebase rechazo el login de Google.");
126
+
127
+ return saveSession({
128
+ provider: "firebase-google",
129
+ email: fb.email || "",
130
+ uid: fb.localId,
131
+ idToken: fb.idToken,
132
+ refreshToken: fb.refreshToken,
133
+ expiresAt: Date.now() + Number(fb.expiresIn || 3600) * 1000,
134
+ });
135
+ }
136
+
137
+ async function refresh(session) {
138
+ const response = await fetch(`https://securetoken.googleapis.com/v1/token?key=${encodeURIComponent(cfg.FIREBASE_API_KEY)}`, {
139
+ method: "POST",
140
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
141
+ body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: session.refreshToken }),
142
+ });
143
+ const body = await response.json();
144
+ if (!response.ok) throw new Error(body.error?.message || "Sesion expirada. Ejecuta: iaoxe login");
145
+ return saveSession({
146
+ ...session,
147
+ idToken: body.id_token,
148
+ refreshToken: body.refresh_token || session.refreshToken,
149
+ expiresAt: Date.now() + Number(body.expires_in || 3600) * 1000,
150
+ });
151
+ }
152
+
153
+ async function getValidToken() {
154
+ const session = loadSession();
155
+ if (!session || !session.refreshToken) throw new Error("No has iniciado sesion. Ejecuta: iaoxe login");
156
+ if (Date.now() > Number(session.expiresAt || 0) - 120000) {
157
+ return (await refresh(session)).idToken;
158
+ }
159
+ return session.idToken;
160
+ }
161
+
162
+ module.exports = { login, refresh, getValidToken, loadSession, clearSession, SESSION_PATH };
package/lib/colors.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+
3
+ // ANSI colors, disabled when not a TTY or NO_COLOR is set.
4
+ const on = process.stdout.isTTY && !process.env.NO_COLOR;
5
+ const wrap = (code) => (s) => (on ? `[${code}m${s}` : String(s));
6
+
7
+ module.exports = {
8
+ green: wrap("32"),
9
+ amber: wrap("33"),
10
+ red: wrap("31"),
11
+ cyan: wrap("36"),
12
+ bold: wrap("1"),
13
+ dim: wrap("2"),
14
+ };
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+
3
+ const { chatCompletion } = require("./router");
4
+ const { managedChat } = require("./managed");
5
+ const { loadSession } = require("./auth");
6
+
7
+ // Decide la via: si hay sesion (creditos IAOXE) y no se forzo BYOK, usa el proxy
8
+ // gestionado; si no, llama al proveedor directo con la clave BYOK del entorno.
9
+ async function complete({ alias, env, messages, stream, onToken, maxTokens, temperature, tools, forceByok }) {
10
+ if (!forceByok && loadSession()) {
11
+ return managedChat({ alias, messages, stream, onToken, maxTokens, temperature, tools });
12
+ }
13
+ return chatCompletion({ alias, env, messages, stream, onToken, maxTokens, temperature, tools });
14
+ }
15
+
16
+ function activeMode({ forceByok } = {}) {
17
+ return !forceByok && loadSession() ? "managed" : "byok";
18
+ }
19
+
20
+ module.exports = { complete, activeMode };
package/lib/config.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+
7
+ // Carga claves desde el entorno y, si existe, ~/.iaoxe/config.json:
8
+ // { "keys": { "ZAI_API_KEY": "...", "ANTHROPIC_API_KEY": "..." } }
9
+ function loadEnv() {
10
+ const env = { ...process.env };
11
+ const cfgPath = path.join(os.homedir(), ".iaoxe", "config.json");
12
+ if (fs.existsSync(cfgPath)) {
13
+ try {
14
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
15
+ if (cfg && cfg.keys && typeof cfg.keys === "object") Object.assign(env, cfg.keys);
16
+ } catch {
17
+ // config invalido: se ignora, se usa solo el entorno
18
+ }
19
+ }
20
+ return env;
21
+ }
22
+
23
+ module.exports = { loadEnv };
package/lib/managed.js ADDED
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+
3
+ const cfg = require("./auth-config");
4
+ const { getValidToken } = require("./auth");
5
+
6
+ // Inferencia gestionada (creditos IAOXE) via el proxy Firebase, con el token de sesion.
7
+ async function managedChat({ alias, messages, stream = false, onToken, maxTokens = 2000, temperature = 0.2, tools }) {
8
+ const token = await getValidToken();
9
+ const body = { model: alias, messages, temperature, max_tokens: maxTokens };
10
+ if (stream) body.stream = true;
11
+ if (tools) body.tools = tools;
12
+
13
+ const response = await fetch(`${cfg.IAOXE_BACKEND_URL}/inference`, {
14
+ method: "POST",
15
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
16
+ body: JSON.stringify(body),
17
+ });
18
+
19
+ if (!response.ok) {
20
+ const text = await response.text();
21
+ let parsed;
22
+ try {
23
+ parsed = JSON.parse(text);
24
+ } catch {
25
+ parsed = null;
26
+ }
27
+ throw new Error(parsed?.error || parsed?.error?.message || `Proxy IAOXE HTTP ${response.status}: ${text.slice(0, 200)}`);
28
+ }
29
+
30
+ if (!stream) {
31
+ const json = await response.json();
32
+ const message = json.choices?.[0]?.message || {};
33
+ return { content: message.content || "", message, usage: json.usage || null };
34
+ }
35
+
36
+ const reader = response.body.getReader();
37
+ const decoder = new TextDecoder();
38
+ let buffer = "";
39
+ let full = "";
40
+ for (;;) {
41
+ const { done, value } = await reader.read();
42
+ if (done) break;
43
+ buffer += decoder.decode(value, { stream: true });
44
+ let index;
45
+ while ((index = buffer.indexOf("\n")) >= 0) {
46
+ const line = buffer.slice(0, index).trim();
47
+ buffer = buffer.slice(index + 1);
48
+ if (!line.startsWith("data:")) continue;
49
+ const data = line.slice(5).trim();
50
+ if (!data || data === "[DONE]") continue;
51
+ try {
52
+ const obj = JSON.parse(data);
53
+ const delta = obj.choices?.[0]?.delta?.content;
54
+ if (delta) {
55
+ full += delta;
56
+ if (onToken) onToken(delta);
57
+ }
58
+ } catch {
59
+ // chunk parcial
60
+ }
61
+ }
62
+ }
63
+ return { content: full };
64
+ }
65
+
66
+ async function billingState() {
67
+ const token = await getValidToken();
68
+ const response = await fetch(`${cfg.IAOXE_BACKEND_URL}/getBillingState`, {
69
+ headers: { Authorization: `Bearer ${token}` },
70
+ });
71
+ const body = await response.json();
72
+ if (!response.ok || body.ok === false) throw new Error(body.error || "No se pudo leer el estado de billing.");
73
+ return body.billing;
74
+ }
75
+
76
+ module.exports = { managedChat, billingState };
package/lib/review.js ADDED
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+
3
+ const { execFileSync } = require("child_process");
4
+ const { complete, activeMode } = require("./complete");
5
+ const c = require("./colors");
6
+
7
+ const REVIEW_PROMPT = [
8
+ "Eres un revisor de codigo senior especializado en seguridad y arquitectura.",
9
+ "Revisas el diff producido por un modelo de coding mas economico antes de que llegue a produccion.",
10
+ "Prioriza: secretos expuestos, autenticacion y permisos, manejo de pagos, validacion de entrada,",
11
+ "inyeccion (SQL/command/XSS), manejo de datos sensibles, correctitud y posibles regresiones.",
12
+ 'Si no hay cambios riesgosos, el verdict es "pass".',
13
+ "Devuelve UNICAMENTE un objeto JSON valido, sin texto adicional, con esta forma exacta:",
14
+ '{ "verdict": "pass"|"changes_requested"|"block", "summary": "1-3 frases",',
15
+ ' "findings": [ { "severity": "critical"|"high"|"medium"|"low", "file": "ruta", "title": "...", "detail": "...", "recommendation": "..." } ],',
16
+ ' "strengths": ["..."] }',
17
+ ].join("\n");
18
+
19
+ function getDiff(staged) {
20
+ const run = (args) => {
21
+ try {
22
+ return execFileSync("git", args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
23
+ } catch {
24
+ return "";
25
+ }
26
+ };
27
+ let diff = staged ? run(["diff", "--cached"]) : run(["diff", "HEAD"]).trim() || run(["diff"]);
28
+ diff = String(diff || "").trim();
29
+ return diff.length > 60000 ? `${diff.slice(0, 60000)}\n... [diff truncado] ...` : diff;
30
+ }
31
+
32
+ function extractJson(text) {
33
+ const start = String(text || "").indexOf("{");
34
+ const end = String(text || "").lastIndexOf("}");
35
+ if (start === -1 || end === -1 || end < start) return null;
36
+ try {
37
+ return JSON.parse(text.slice(start, end + 1));
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ const SEVERITY = { critical: c.red("CRITICO"), high: c.red("ALTO"), medium: c.amber("MEDIO"), low: c.dim("BAJO") };
44
+ const VERDICT = {
45
+ pass: c.green("APROBADO"),
46
+ changes_requested: c.amber("CAMBIOS SUGERIDOS"),
47
+ block: c.red("BLOQUEADO"),
48
+ };
49
+
50
+ async function review({ staged = false, model = "claude-review", env, forceByok = false }) {
51
+ const diff = getDiff(staged);
52
+ if (!diff) {
53
+ console.log(c.dim(`No hay cambios ${staged ? "en stage" : "sin commitear"} para revisar.`));
54
+ return 0;
55
+ }
56
+
57
+ process.stderr.write(c.dim(`Revisando con ${model} (${activeMode({ forceByok })})...\n`));
58
+ const { content, usage } = await complete({
59
+ alias: model,
60
+ env,
61
+ forceByok,
62
+ temperature: 0.1,
63
+ maxTokens: 2000,
64
+ messages: [
65
+ { role: "system", content: REVIEW_PROMPT },
66
+ { role: "user", content: `Revisa de forma critica este diff y responde solo con el JSON pedido:\n\n${diff}` },
67
+ ],
68
+ });
69
+
70
+ const parsed = extractJson(content);
71
+ if (!parsed) {
72
+ console.log(c.amber("El modelo no devolvio un reporte estructurado:\n"));
73
+ console.log(content);
74
+ return 1;
75
+ }
76
+
77
+ console.log(`\n${c.bold("Veredicto:")} ${VERDICT[parsed.verdict] || parsed.verdict || "—"}`);
78
+ if (parsed.summary) console.log(c.dim(parsed.summary));
79
+
80
+ const findings = Array.isArray(parsed.findings) ? parsed.findings : [];
81
+ if (findings.length) {
82
+ console.log("");
83
+ for (const f of findings) {
84
+ const sev = SEVERITY[String(f.severity || "low").toLowerCase()] || String(f.severity || "");
85
+ console.log(`${sev} ${c.bold(f.title || "Hallazgo")}${f.file ? c.dim(` (${f.file})`) : ""}`);
86
+ if (f.detail) console.log(` ${f.detail}`);
87
+ if (f.recommendation) console.log(` ${c.cyan("→")} ${f.recommendation}`);
88
+ }
89
+ } else {
90
+ console.log(c.green("\nSin hallazgos de riesgo en este cambio."));
91
+ }
92
+
93
+ if (usage) {
94
+ console.log(c.dim(`\nTokens: ${Number(usage.prompt_tokens || 0)} in / ${Number(usage.completion_tokens || 0)} out`));
95
+ }
96
+
97
+ // exit code util para CI: 2 si bloquea, 1 si pide cambios, 0 si pasa.
98
+ if (parsed.verdict === "block") return 2;
99
+ if (parsed.verdict === "changes_requested") return 1;
100
+ return 0;
101
+ }
102
+
103
+ module.exports = { review };
package/lib/router.js ADDED
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+
3
+ // Misma tabla de routing que el desktop/proxy: alias -> proveedor OpenAI-compatible.
4
+ const ROUTES = {
5
+ "glm-coding": { baseURL: "https://api.z.ai/api/coding/paas/v4", envKey: "ZAI_API_KEY", model: "glm-5.2" },
6
+ "deepseek-coding": { baseURL: "https://api.deepseek.com", envKey: "DEEPSEEK_API_KEY", model: "deepseek-chat" },
7
+ "claude-review": { baseURL: "https://api.anthropic.com/v1", envKey: "ANTHROPIC_API_KEY", model: "claude-sonnet-4-6" },
8
+ "claude-opus": { baseURL: "https://api.anthropic.com/v1", envKey: "ANTHROPIC_API_KEY", model: "claude-opus-4-1" },
9
+ "openai-fallback": { baseURL: "https://api.openai.com/v1", envKey: "OPENAI_API_KEY", model: "gpt-4.1" },
10
+ "gemini-vision": { baseURL: "https://generativelanguage.googleapis.com/v1beta/openai", envKey: "GEMINI_API_KEY", model: "gemini-2.0-flash" },
11
+ };
12
+
13
+ function routeAliases() {
14
+ return Object.keys(ROUTES);
15
+ }
16
+
17
+ function resolveRoute(alias) {
18
+ return ROUTES[String(alias || "").trim()] || null;
19
+ }
20
+
21
+ // Llama a un modelo (OpenAI-compatible) con la clave BYOK del entorno.
22
+ // stream:true -> invoca onToken(delta) y devuelve { content }.
23
+ async function chatCompletion({ alias, env, messages, stream = false, onToken, maxTokens = 2000, temperature = 0.2, tools }) {
24
+ const route = resolveRoute(alias);
25
+ if (!route) throw new Error(`Modelo desconocido: ${alias}. Usa "iaoxe models".`);
26
+ const key = String((env && env[route.envKey]) || "").trim();
27
+ if (!key) {
28
+ throw new Error(
29
+ `Falta ${route.envKey} para usar "${alias}". Exporta la variable o agregala en ~/.iaoxe/config.json.`,
30
+ );
31
+ }
32
+
33
+ const body = { model: route.model, messages, temperature, max_tokens: maxTokens };
34
+ if (stream) body.stream = true;
35
+ if (tools) body.tools = tools;
36
+
37
+ const response = await fetch(`${route.baseURL}/chat/completions`, {
38
+ method: "POST",
39
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
40
+ body: JSON.stringify(body),
41
+ });
42
+
43
+ if (!response.ok) {
44
+ const text = await response.text();
45
+ throw new Error(`Proveedor (${alias}) respondio HTTP ${response.status}: ${text.slice(0, 200)}`);
46
+ }
47
+
48
+ if (!stream) {
49
+ const json = await response.json();
50
+ const message = json.choices?.[0]?.message || {};
51
+ return { content: message.content || "", message, usage: json.usage || null };
52
+ }
53
+
54
+ const reader = response.body.getReader();
55
+ const decoder = new TextDecoder();
56
+ let buffer = "";
57
+ let full = "";
58
+ for (;;) {
59
+ const { done, value } = await reader.read();
60
+ if (done) break;
61
+ buffer += decoder.decode(value, { stream: true });
62
+ let index;
63
+ while ((index = buffer.indexOf("\n")) >= 0) {
64
+ const line = buffer.slice(0, index).trim();
65
+ buffer = buffer.slice(index + 1);
66
+ if (!line.startsWith("data:")) continue;
67
+ const data = line.slice(5).trim();
68
+ if (!data || data === "[DONE]") continue;
69
+ try {
70
+ const obj = JSON.parse(data);
71
+ const delta = obj.choices?.[0]?.delta?.content;
72
+ if (delta) {
73
+ full += delta;
74
+ if (onToken) onToken(delta);
75
+ }
76
+ } catch {
77
+ // chunk parcial
78
+ }
79
+ }
80
+ }
81
+ return { content: full };
82
+ }
83
+
84
+ module.exports = { ROUTES, routeAliases, resolveRoute, chatCompletion };
package/lib/run.js ADDED
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+
3
+ const { complete, activeMode } = require("./complete");
4
+ const { agentRun } = require("./agent");
5
+ const c = require("./colors");
6
+
7
+ // iaoxe run: por defecto es AGENTICO (lee y edita archivos del repo via tools).
8
+ // --print: modo antiguo, solo transmite la respuesta sin tocar archivos.
9
+ async function run({ task, model = "glm-coding", env, forceByok = false, print = false, dryRun = false }) {
10
+ if (!task || !task.trim()) {
11
+ console.error(c.amber('Uso: iaoxe run "describe la tarea" [--print] [--dry-run]'));
12
+ return 1;
13
+ }
14
+
15
+ if (print) {
16
+ process.stderr.write(c.dim(`(${model} · ${activeMode({ forceByok })})\n`));
17
+ await complete({
18
+ alias: model,
19
+ env,
20
+ forceByok,
21
+ stream: true,
22
+ maxTokens: 2000,
23
+ temperature: 0.3,
24
+ messages: [
25
+ {
26
+ role: "system",
27
+ content: "Eres un asistente de coding directo y conciso. Devuelve codigo claro, listo para usar.",
28
+ },
29
+ { role: "user", content: task },
30
+ ],
31
+ onToken: (delta) => process.stdout.write(delta),
32
+ });
33
+ process.stdout.write("\n");
34
+ return 0;
35
+ }
36
+
37
+ return agentRun({ task, model, env, forceByok, dryRun });
38
+ }
39
+
40
+ module.exports = { run };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "iaoxe-cli",
3
+ "version": "0.1.0",
4
+ "description": "IAOXE orchestration in your terminal: GLM writes, Claude reviews, with cost control. Agentic run + critical review, managed credits or BYOK.",
5
+ "bin": {
6
+ "iaoxe": "bin/iaoxe.js"
7
+ },
8
+ "type": "commonjs",
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "lib",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "ai",
19
+ "coding",
20
+ "cli",
21
+ "agent",
22
+ "code-review",
23
+ "llm",
24
+ "orchestration",
25
+ "iaoxe"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/Cristianrjs01/iaoxe.git",
30
+ "directory": "iaoxe-cli"
31
+ },
32
+ "homepage": "https://iaoxe.com",
33
+ "bugs": {
34
+ "url": "https://github.com/Cristianrjs01/iaoxe/issues"
35
+ },
36
+ "author": "IAOXE LLC",
37
+ "license": "UNLICENSED",
38
+ "scripts": {
39
+ "lint": "node --check bin/iaoxe.js && node --check lib/router.js && node --check lib/config.js && node --check lib/colors.js && node --check lib/review.js && node --check lib/run.js && node --check lib/auth.js && node --check lib/auth-config.js && node --check lib/managed.js && node --check lib/complete.js && node --check lib/agent.js",
40
+ "prepublishOnly": "npm run lint"
41
+ }
42
+ }