agent-mp 0.4.14 → 0.5.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.
@@ -233,7 +233,7 @@ function detectModels(cliName) {
233
233
  }
234
234
  catch { }
235
235
  }
236
- return [`run: ${cliName} --login`];
236
+ return ['coder-model'];
237
237
  }
238
238
  default: return ['default'];
239
239
  }
@@ -1136,11 +1136,11 @@ export async function runRepl(resumeSession) {
1136
1136
  * Prompt estricto: cada rol solo hace lo que debe, sin extras.
1137
1137
  */
1138
1138
  export async function runRole(role, arg, model) {
1139
- const init = await initCoordinator();
1140
- if (!init)
1141
- process.exit(1);
1142
- const { coordinatorCmd } = init;
1143
- const rl = init.rl;
1139
+ const { PKG_NAME, getConfigDir } = await import('../utils/config.js');
1140
+ // Detect if running a native role binary directly (agent-orch, agent-impl, etc.)
1141
+ // In that case, skip REPL and run headless — no coordinator needed
1142
+ const ROLE_BINS = new Set(['agent-orch', 'agent-impl', 'agent-rev', 'agent-explorer']);
1143
+ const isNativeBin = ROLE_BINS.has(PKG_NAME);
1144
1144
  const dir = process.cwd();
1145
1145
  let config;
1146
1146
  try {
@@ -1148,13 +1148,11 @@ export async function runRole(role, arg, model) {
1148
1148
  }
1149
1149
  catch {
1150
1150
  console.log(chalk.red(' No project config found. Run /setup first.'));
1151
- rl.close();
1152
1151
  process.exit(1);
1153
1152
  }
1154
1153
  // Validate roles are configured
1155
1154
  if (!config.roles?.orchestrator?.cli || !config.roles?.implementor?.cli || !config.roles?.reviewer?.cli) {
1156
1155
  console.log(chalk.red(' Roles not configured. Run setup first.'));
1157
- rl.close();
1158
1156
  process.exit(1);
1159
1157
  }
1160
1158
  // Override model if passed via --model flag
@@ -1170,7 +1168,60 @@ export async function runRole(role, arg, model) {
1170
1168
  r.cmd = r.cmd.replace(/(-m|--model)\s+\S+/, `$1 ${model}`);
1171
1169
  }
1172
1170
  }
1173
- console.log(chalk.bold.cyan(`\n Agent-mp — Rol: ${role.toUpperCase()}${model ? ` (${model})` : ''}\n`));
1171
+ console.log(chalk.bold.cyan(`\n ${PKG_NAME} — Rol: ${role.toUpperCase()}${model ? ` (${model})` : ''}\n`));
1172
+ if (isNativeBin) {
1173
+ // Headless mode: create a dummy RL (no stdin), run engine directly
1174
+ const { Readable } = await import('stream');
1175
+ const emptyStream = new Readable({ read() { this.push(null); } });
1176
+ const dummyRl = readline.createInterface({ input: emptyStream, output: process.stdout });
1177
+ const engine = new AgentEngine(config, dir, '', dummyRl);
1178
+ try {
1179
+ switch (role.toLowerCase()) {
1180
+ case 'orchestrator':
1181
+ case 'orch': {
1182
+ const result = await engine.runOrchestrator(arg);
1183
+ log.ok(`Task ID: ${result.taskId}`);
1184
+ break;
1185
+ }
1186
+ case 'implementor':
1187
+ case 'impl': {
1188
+ const taskDir = path.join(dir, '.agent', 'tasks', arg);
1189
+ const plan = await readJson(path.join(taskDir, 'plan.json'));
1190
+ await engine.runImplementor(arg, plan);
1191
+ break;
1192
+ }
1193
+ case 'reviewer':
1194
+ case 'rev': {
1195
+ const taskDir = path.join(dir, '.agent', 'tasks', arg);
1196
+ const plan = await readJson(path.join(taskDir, 'plan.json'));
1197
+ const progress = await readJson(path.join(taskDir, 'progress.json'));
1198
+ await engine.runReviewer(arg, plan, progress);
1199
+ break;
1200
+ }
1201
+ case 'explorer':
1202
+ case 'exp': {
1203
+ const result = await engine.runExplorer(arg);
1204
+ log.ok(`Explorer: ${result.substring(0, 100)}...`);
1205
+ break;
1206
+ }
1207
+ default:
1208
+ console.log(chalk.red(` Unknown role: ${role}. Use: orchestrator, implementor, reviewer, explorer`));
1209
+ process.exit(1);
1210
+ }
1211
+ }
1212
+ catch (err) {
1213
+ console.log(chalk.red(` Error: ${err.message}`));
1214
+ process.exit(1);
1215
+ }
1216
+ dummyRl.close();
1217
+ return;
1218
+ }
1219
+ // REPL mode: needs coordinator
1220
+ const init = await initCoordinator();
1221
+ if (!init)
1222
+ process.exit(1);
1223
+ const { coordinatorCmd } = init;
1224
+ const rl = init.rl;
1174
1225
  const engine = new AgentEngine(config, dir, coordinatorCmd, rl);
1175
1226
  try {
1176
1227
  switch (role.toLowerCase()) {
@@ -1195,6 +1246,12 @@ export async function runRole(role, arg, model) {
1195
1246
  await engine.runReviewer(arg, plan, progress);
1196
1247
  break;
1197
1248
  }
1249
+ case 'explorer':
1250
+ case 'exp': {
1251
+ const result = await engine.runExplorer(arg);
1252
+ log.ok(`Explorer: ${result.substring(0, 100)}...`);
1253
+ break;
1254
+ }
1198
1255
  case 'coordinator':
1199
1256
  case 'coord': {
1200
1257
  console.log(chalk.yellow(' Coordinator mode requires interactive REPL.'));
@@ -1202,7 +1259,7 @@ export async function runRole(role, arg, model) {
1202
1259
  process.exit(1);
1203
1260
  }
1204
1261
  default:
1205
- console.log(chalk.red(` Unknown role: ${role}. Use: orchestrator, implementor, reviewer`));
1262
+ console.log(chalk.red(` Unknown role: ${role}. Use: orchestrator, implementor, reviewer, explorer`));
1206
1263
  process.exit(1);
1207
1264
  }
1208
1265
  }
@@ -73,7 +73,7 @@ function detectModels(cliName) {
73
73
  }
74
74
  catch { }
75
75
  }
76
- return [`run: ${cliName} --login`];
76
+ return ['coder-model'];
77
77
  }
78
78
  default:
79
79
  return ['default'];
package/dist/index.js CHANGED
@@ -137,7 +137,7 @@ if (nativeRole) {
137
137
  if (modelIdx !== -1 && args[modelIdx + 1]) {
138
138
  model = args[modelIdx + 1];
139
139
  }
140
- const taskArg = args.filter((a, i) => !a.startsWith('-') && i !== modelIdx + 1).join(' ').trim();
140
+ const taskArg = args.filter((a, i) => !a.startsWith('-') && (modelIdx === -1 || i !== modelIdx + 1)).join(' ').trim();
141
141
  if (!taskArg) {
142
142
  console.log(chalk.bold.cyan(`\n ${PKG_NAME} — ${nativeRole} agent\n`));
143
143
  console.log(chalk.dim(` Usage: ${PKG_NAME} [--model <model>] "<task>"`));
@@ -42,9 +42,10 @@ async function loadToken() {
42
42
  };
43
43
  if (!token.accessToken)
44
44
  return null;
45
- // Refresh proactivo: si vence en menos de 2 minutos (o ya venció)
46
- const TWO_MIN = 2 * 60 * 1000;
47
- if (token.expiresAt - Date.now() < TWO_MIN && token.refreshToken) {
45
+ // Refresh proactivo: si vence en menos de 5 minutos (o ya venció)
46
+ // Mismo threshold que el CLI original de qwen
47
+ const FIVE_MIN = 5 * 60 * 1000;
48
+ if (token.expiresAt - Date.now() < FIVE_MIN && token.refreshToken) {
48
49
  const refreshed = await doRefreshToken(token.refreshToken);
49
50
  if (refreshed) {
50
51
  await saveToken(refreshed);
@@ -253,45 +254,41 @@ export async function getQwenAccessToken() {
253
254
  return token?.accessToken || null;
254
255
  }
255
256
  async function callQwenAPIWithToken(token, prompt, model, onData) {
256
- // Use resource_url from token (e.g. "portal.qwen.ai"), fallback to DashScope
257
+ // Build base URL from token's resource_url (same logic as qwen CLI original)
257
258
  const rawHost = token.resourceUrl || 'dashscope.aliyuncs.com/compatible-mode';
258
- const host = rawHost.startsWith('http') ? rawHost : `https://${rawHost}`;
259
- const baseUrl = host.endsWith('/v1') ? host : `${host}/v1`;
259
+ const baseUrl = rawHost.startsWith('http')
260
+ ? (rawHost.endsWith('/v1') ? rawHost : rawHost.replace(/\/$/, '') + '/v1')
261
+ : `https://${rawHost}/v1`;
260
262
  const useStream = !!onData;
261
- const userAgent = `QwenCode/0.14.2 (${process.platform}; ${process.arch})`;
262
- // portal.qwen.ai requires: content parts format + system message (plain strings → 400, no system → 400)
263
- const toContentParts = (text) => [{ type: 'text', text }];
263
+ // portal.qwen.ai requires: content as array of {type, text} objects + system message
264
+ // DashScope accepts: plain string content
265
+ const isPortalQwen = rawHost.includes('portal.qwen.ai') || rawHost.includes('chat.qwen.ai');
266
+ const toContent = (text) => isPortalQwen ? [{ type: 'text', text }] : text;
264
267
  const response = await fetch(`${baseUrl}/chat/completions`, {
265
268
  method: 'POST',
266
269
  headers: {
267
270
  'Authorization': `Bearer ${token.accessToken}`,
268
271
  'Content-Type': 'application/json',
269
272
  'Accept': 'application/json',
270
- 'User-Agent': userAgent,
271
- 'x-dashscope-authtype': 'qwen-oauth',
272
- 'x-dashscope-cachecontrol': 'enable',
273
- 'x-dashscope-useragent': userAgent,
274
- 'x-stainless-lang': 'js',
275
- 'x-stainless-package-version': '5.11.0',
276
- 'x-stainless-os': process.platform,
277
- 'x-stainless-arch': process.arch,
278
- 'x-stainless-runtime': 'node',
279
- 'x-stainless-runtime-version': process.version,
280
- 'x-stainless-retry-count': '0',
273
+ 'User-Agent': `QwenCode/0.14.2 (${process.platform}; ${process.arch})`,
274
+ 'X-DashScope-AuthType': 'qwen-oauth',
275
+ 'X-DashScope-CacheControl': 'enable',
276
+ 'X-DashScope-UserAgent': `QwenCode/0.14.2 (${process.platform}; ${process.arch})`,
281
277
  },
282
278
  body: JSON.stringify({
283
279
  model: model || 'coder-model',
284
280
  messages: [
285
- { role: 'system', content: toContentParts('You are a helpful coding assistant.') },
286
- { role: 'user', content: toContentParts(prompt) },
281
+ { role: 'system', content: toContent('You are a helpful coding assistant.') },
282
+ { role: 'user', content: toContent(prompt) },
287
283
  ],
288
284
  stream: useStream,
289
285
  }),
290
286
  });
291
287
  if (!response.ok) {
292
288
  const errorText = await response.text();
293
- if (response.status === 401)
289
+ if (response.status === 401 || (response.status === 429 && errorText.includes('insufficient_quota'))) {
294
290
  throw new Error(`QWEN_AUTH_EXPIRED: ${errorText}`);
291
+ }
295
292
  throw new Error(`Qwen API error: ${response.status} - ${errorText}`);
296
293
  }
297
294
  if (!useStream) {
@@ -380,8 +377,9 @@ export async function callQwenAPIFromCreds(prompt, model, credsPath, onData) {
380
377
  if (!token.accessToken) {
381
378
  throw new Error(`Invalid credentials at ${credsPath}. Run: ${cliName} --login`);
382
379
  }
383
- const TWO_MIN = 2 * 60 * 1000;
384
- if (token.expiresAt - Date.now() < TWO_MIN && token.refreshToken) {
380
+ // Refresh proactivo: mismo threshold de 5 minutos que el CLI original
381
+ const FIVE_MIN = 5 * 60 * 1000;
382
+ if (token.expiresAt - Date.now() < FIVE_MIN && token.refreshToken) {
385
383
  const refreshed = await doRefreshToken(token.refreshToken);
386
384
  if (refreshed) {
387
385
  token = refreshed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-mp",
3
- "version": "0.4.14",
3
+ "version": "0.5.0",
4
4
  "description": "Deterministic multi-agent CLI orchestrator — plan, code, review",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,12 +0,0 @@
1
- interface QwenMessage {
2
- role: 'system' | 'user' | 'assistant';
3
- content: string;
4
- }
5
- export declare function qwenLogin(): Promise<boolean>;
6
- export declare function qwenAuthStatus(): Promise<{
7
- authenticated: boolean;
8
- email?: string;
9
- }>;
10
- export declare function qwenChat(messages: QwenMessage[], model?: string): Promise<string>;
11
- export declare function qwenAsk(prompt: string, model?: string): Promise<string>;
12
- export {};
package/dist/api/qwen.js DELETED
@@ -1,150 +0,0 @@
1
- import axios from 'axios';
2
- import * as fs from 'fs/promises';
3
- import * as path from 'path';
4
- import * as os from 'os';
5
- import open from 'open';
6
- const QWEN_API_BASE = 'https://dashscope-intl.aliyuncs.com/api/v1';
7
- const QWEN_AUTH_URL = 'https://oauth.aliyun.com/v1/oauth/authorize';
8
- const CLIENT_ID = 'your_client_id'; // Reemplazar con el client ID real de Qwen
9
- const REDIRECT_URI = 'http://localhost:3000/callback';
10
- let tokenCache = null;
11
- async function getTokenPath() {
12
- const homeDir = os.homedir();
13
- const dir = path.join(homeDir, '.agent');
14
- await fs.mkdir(dir, { recursive: true });
15
- return path.join(dir, 'qwen-token.json');
16
- }
17
- async function loadToken() {
18
- try {
19
- const tokenPath = await getTokenPath();
20
- const content = await fs.readFile(tokenPath, 'utf-8');
21
- const token = JSON.parse(content);
22
- if (token.expiresAt > Date.now()) {
23
- return token;
24
- }
25
- // Token expirado, intentar refresh
26
- if (token.refreshToken) {
27
- return refreshAccessToken(token.refreshToken);
28
- }
29
- return null;
30
- }
31
- catch {
32
- return null;
33
- }
34
- }
35
- async function saveToken(token) {
36
- const tokenPath = await getTokenPath();
37
- await fs.writeFile(tokenPath, JSON.stringify(token, null, 2), 'utf-8');
38
- }
39
- async function refreshAccessToken(refreshToken) {
40
- try {
41
- const response = await axios.post('https://oauth.aliyun.com/v1/oauth/token', new URLSearchParams({
42
- grant_type: 'refresh_token',
43
- refresh_token: refreshToken,
44
- client_id: CLIENT_ID,
45
- }), {
46
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
47
- });
48
- const token = {
49
- accessToken: response.data.access_token,
50
- refreshToken: response.data.refresh_token || refreshToken,
51
- expiresAt: Date.now() + (response.data.expires_in || 3600) * 1000,
52
- };
53
- await saveToken(token);
54
- return token;
55
- }
56
- catch (error) {
57
- console.error('Error refreshing token:', error);
58
- return null;
59
- }
60
- }
61
- export async function qwenLogin() {
62
- const authUrl = `${QWEN_AUTH_URL}?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&response_type=code&scope=api`;
63
- console.log('Abriendo navegador para autenticación con Qwen...');
64
- console.log('URL:', authUrl);
65
- await open(authUrl);
66
- console.log('');
67
- console.log('Por favor completá la autenticación en el navegador.');
68
- console.log('Cuando obtengas el código de autorización, ingresalo abajo:');
69
- return new Promise((resolve) => {
70
- const readline = await import('readline');
71
- const rl = readline.createInterface({
72
- input: process.stdin,
73
- output: process.stdout,
74
- });
75
- rl.question('Código de autorización: ', async (code) => {
76
- try {
77
- const response = await axios.post('https://oauth.aliyun.com/v1/oauth/token', new URLSearchParams({
78
- grant_type: 'authorization_code',
79
- code,
80
- redirect_uri: REDIRECT_URI,
81
- client_id: CLIENT_ID,
82
- }), {
83
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
84
- });
85
- const token = {
86
- accessToken: response.data.access_token,
87
- refreshToken: response.data.refresh_token,
88
- expiresAt: Date.now() + (response.data.expires_in || 3600) * 1000,
89
- };
90
- await saveToken(token);
91
- console.log('¡Autenticación exitosa!');
92
- rl.close();
93
- resolve(true);
94
- }
95
- catch (error) {
96
- console.error('Error en autenticación:', error);
97
- rl.close();
98
- resolve(false);
99
- }
100
- });
101
- });
102
- }
103
- export async function qwenAuthStatus() {
104
- const token = await loadToken();
105
- if (token) {
106
- return { authenticated: true };
107
- }
108
- return { authenticated: false };
109
- }
110
- export async function qwenChat(messages, model = 'qwen-coder') {
111
- const token = await loadToken();
112
- if (!token) {
113
- throw new Error('No hay token de Qwen. Ejecutá /qwen-login primero.');
114
- }
115
- try {
116
- const response = await axios.post(`${QWEN_API_BASE}/services/aigc/text-generation/generation`, {
117
- model,
118
- input: {
119
- messages,
120
- },
121
- parameters: {
122
- result_format: 'text',
123
- },
124
- }, {
125
- headers: {
126
- 'Authorization': `Bearer ${token.accessToken}`,
127
- 'Content-Type': 'application/json',
128
- },
129
- });
130
- return response.data.output.text;
131
- }
132
- catch (error) {
133
- if (error.response?.status === 401) {
134
- // Token inválido, intentar refresh
135
- const newToken = token.refreshToken ? await refreshAccessToken(token.refreshToken) : null;
136
- if (newToken) {
137
- // Reintentar con el nuevo token
138
- return qwenChat(messages, model);
139
- }
140
- }
141
- throw error;
142
- }
143
- }
144
- export async function qwenAsk(prompt, model = 'qwen-coder') {
145
- const messages = [
146
- { role: 'system', content: 'Sos un asistente útil de programación.' },
147
- { role: 'user', content: prompt },
148
- ];
149
- return qwenChat(messages, model);
150
- }
@@ -1,31 +0,0 @@
1
- import { Command } from 'commander';
2
- declare const CONFIG_DIR: string;
3
- declare const CONFIG_FILE: string;
4
- interface AuthEntry {
5
- provider: string;
6
- method: 'oauth' | 'apikey';
7
- accessToken?: string;
8
- refreshToken?: string;
9
- expiresAt?: number;
10
- apiKey?: string;
11
- email?: string;
12
- }
13
- interface AuthStore {
14
- entries: AuthEntry[];
15
- activeProvider?: string;
16
- }
17
- interface CliConfig {
18
- roles: Record<string, {
19
- provider: string;
20
- model: string;
21
- }>;
22
- deliberation: {
23
- enabled: boolean;
24
- max_rounds: number;
25
- };
26
- }
27
- declare function loadAuth(): Promise<AuthStore>;
28
- declare function loadCliConfig(): Promise<CliConfig>;
29
- declare function saveCliConfig(cfg: CliConfig): Promise<void>;
30
- export declare function authCommand(program: Command): void;
31
- export { loadAuth, loadCliConfig, saveCliConfig, CONFIG_DIR, CONFIG_FILE };
@@ -1,255 +0,0 @@
1
- import * as readline from 'readline';
2
- import * as path from 'path';
3
- import * as fs from 'fs/promises';
4
- import { createServer } from 'http';
5
- import { createHash, randomBytes } from 'crypto';
6
- import open from 'open';
7
- import chalk from 'chalk';
8
- import { writeJson, ensureDir } from '../utils/fs.js';
9
- const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.agent-cli');
10
- const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
11
- const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
12
- // ── OAuth providers ────────────────────────────────────────────────────────
13
- const OAUTH_PROVIDERS = {
14
- opencode: {
15
- authorizeUrl: 'https://accounts.opencode.ai/oauth2/auth',
16
- tokenUrl: 'https://accounts.opencode.ai/oauth2/token',
17
- clientId: 'claw_code',
18
- scopes: 'openid email profile offline_access',
19
- name: 'Opencode',
20
- },
21
- google: {
22
- authorizeUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
23
- tokenUrl: 'https://oauth2.googleapis.com/token',
24
- clientId: '',
25
- scopes: 'openid email profile',
26
- name: 'Google',
27
- },
28
- };
29
- async function loadAuth() {
30
- try {
31
- const content = await fs.readFile(AUTH_FILE, 'utf-8');
32
- return JSON.parse(content);
33
- }
34
- catch {
35
- return { entries: [] };
36
- }
37
- }
38
- async function saveAuth(auth) {
39
- await ensureDir(CONFIG_DIR);
40
- await writeJson(AUTH_FILE, auth);
41
- }
42
- async function loadCliConfig() {
43
- try {
44
- const content = await fs.readFile(CONFIG_FILE, 'utf-8');
45
- return JSON.parse(content);
46
- }
47
- catch {
48
- return { roles: {}, deliberation: { enabled: false, max_rounds: 4 } };
49
- }
50
- }
51
- async function saveCliConfig(cfg) {
52
- await ensureDir(CONFIG_DIR);
53
- await writeJson(CONFIG_FILE, cfg);
54
- }
55
- function base64url(buf) {
56
- return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
57
- }
58
- async function oauthFlow(providerId) {
59
- const provider = OAUTH_PROVIDERS[providerId];
60
- if (!provider)
61
- throw new Error(`Unknown OAuth provider: ${providerId}`);
62
- const port = 9800 + Math.floor(Math.random() * 1000);
63
- const state = base64url(randomBytes(32));
64
- const codeVerifier = base64url(randomBytes(32));
65
- const codeChallenge = base64url(createHash('sha256').update(codeVerifier).digest());
66
- const redirectUri = `http://127.0.0.1:${port}/callback`;
67
- const authUrl = new URL(provider.authorizeUrl);
68
- authUrl.searchParams.set('response_type', 'code');
69
- authUrl.searchParams.set('client_id', provider.clientId);
70
- authUrl.searchParams.set('redirect_uri', redirectUri);
71
- authUrl.searchParams.set('scope', provider.scopes);
72
- authUrl.searchParams.set('state', state);
73
- authUrl.searchParams.set('code_challenge', codeChallenge);
74
- authUrl.searchParams.set('code_challenge_method', 'S256');
75
- console.log(chalk.dim(` Opening browser for ${provider.name} login...`));
76
- await open(authUrl.toString());
77
- return new Promise((resolve, reject) => {
78
- const server = createServer((req, res) => {
79
- const url = new URL(req.url, `http://127.0.0.1:${port}`);
80
- if (url.pathname !== '/callback') {
81
- res.writeHead(404);
82
- res.end();
83
- return;
84
- }
85
- const returnedState = url.searchParams.get('state');
86
- const code = url.searchParams.get('code');
87
- const error = url.searchParams.get('error');
88
- if (error) {
89
- res.writeHead(400, { 'Content-Type': 'text/html' });
90
- res.end(`<h1>Login failed</h1><p>${error}</p>`);
91
- server.close();
92
- reject(new Error(`OAuth error: ${error}`));
93
- return;
94
- }
95
- if (returnedState !== state) {
96
- res.writeHead(400, { 'Content-Type': 'text/html' });
97
- res.end('<h1>Invalid state</h1>');
98
- server.close();
99
- reject(new Error('State mismatch'));
100
- return;
101
- }
102
- if (!code) {
103
- res.writeHead(400, { 'Content-Type': 'text/html' });
104
- res.end('<h1>No code</h1>');
105
- server.close();
106
- reject(new Error('No authorization code'));
107
- return;
108
- }
109
- res.writeHead(200, { 'Content-Type': 'text/html' });
110
- res.end(`
111
- <html><body style="font-family:system-ui;text-align:center;padding:60px">
112
- <h1 style="color:#06b6d4">✓ Login successful</h1>
113
- <p>You can close this tab and return to the terminal.</p>
114
- </body></html>
115
- `);
116
- server.close();
117
- // Exchange code for tokens
118
- exchangeToken(provider, code, redirectUri, codeVerifier).then(resolve).catch(reject);
119
- });
120
- server.listen(port, '127.0.0.1', () => {
121
- console.log(chalk.blue(` → Waiting for callback on port ${port}...`));
122
- });
123
- server.on('error', (err) => {
124
- if (err.code === 'EADDRINUSE') {
125
- reject(new Error(`Port ${port} is in use. Try again.`));
126
- }
127
- else {
128
- reject(err);
129
- }
130
- });
131
- });
132
- }
133
- async function exchangeToken(provider, code, redirectUri, codeVerifier) {
134
- const params = new URLSearchParams({
135
- grant_type: 'authorization_code',
136
- client_id: provider.clientId,
137
- code,
138
- redirect_uri: redirectUri,
139
- code_verifier: codeVerifier,
140
- });
141
- const res = await fetch(provider.tokenUrl, {
142
- method: 'POST',
143
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
144
- body: params.toString(),
145
- });
146
- if (!res.ok) {
147
- const text = await res.text();
148
- throw new Error(`Token exchange failed (${res.status}): ${text}`);
149
- }
150
- const data = await res.json();
151
- // Decode email from id_token if available
152
- let email;
153
- if (data.id_token) {
154
- try {
155
- const payload = JSON.parse(Buffer.from(data.id_token.split('.')[1], 'base64').toString());
156
- email = payload.email;
157
- }
158
- catch { }
159
- }
160
- return {
161
- provider: 'opencode',
162
- method: 'oauth',
163
- accessToken: data.access_token,
164
- refreshToken: data.refresh_token,
165
- expiresAt: data.expires_in ? Date.now() + data.expires_in * 1000 : undefined,
166
- email,
167
- };
168
- }
169
- function ask(rl, q) {
170
- return new Promise((resolve) => rl.question(q, resolve));
171
- }
172
- export function authCommand(program) {
173
- const auth = program.command('auth')
174
- .description('Manage authentication');
175
- auth
176
- .command('login')
177
- .description('Login to a provider (opens browser)')
178
- .option('-p, --provider <name>', 'Provider name (opencode)')
179
- .action(async (opts) => {
180
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
181
- console.log(chalk.bold.cyan('\n Agent CLI — Login\n'));
182
- let providerId = opts.provider;
183
- if (!providerId) {
184
- console.log(chalk.dim(' Available providers:'));
185
- for (const [id, p] of Object.entries(OAUTH_PROVIDERS)) {
186
- console.log(chalk.dim(` ${id} — ${p.name}`));
187
- }
188
- providerId = await ask(rl, '\n Provider: ');
189
- }
190
- providerId = providerId.trim();
191
- try {
192
- const entry = await oauthFlow(providerId);
193
- const auth = await loadAuth();
194
- // Remove existing entry for same provider
195
- auth.entries = auth.entries.filter((e) => e.provider !== entry.provider);
196
- auth.entries.push(entry);
197
- auth.activeProvider = entry.provider;
198
- await saveAuth(auth);
199
- console.log(chalk.green(`\n ✓ Logged in as ${entry.email || 'user'} via ${entry.provider}`));
200
- console.log(chalk.dim(' Auth saved to ~/.agent-cli/auth.json'));
201
- }
202
- catch (err) {
203
- console.log(chalk.red(`\n ✗ Login failed: ${err.message}`));
204
- }
205
- rl.close();
206
- });
207
- auth
208
- .command('logout')
209
- .description('Logout from current provider')
210
- .action(async () => {
211
- const auth = await loadAuth();
212
- if (!auth.activeProvider || auth.entries.length === 0) {
213
- console.log(chalk.yellow(' Not logged in'));
214
- return;
215
- }
216
- auth.entries = auth.entries.filter((e) => e.provider !== auth.activeProvider);
217
- auth.activeProvider = undefined;
218
- await saveAuth(auth);
219
- console.log(chalk.green(' ✓ Logged out'));
220
- });
221
- auth
222
- .command('status')
223
- .description('Show current auth status')
224
- .action(async () => {
225
- const auth = await loadAuth();
226
- console.log(chalk.bold('\n Auth Status'));
227
- console.log(chalk.dim(' ──────────────────────────'));
228
- if (auth.entries.length === 0) {
229
- console.log(chalk.yellow(' Not logged in to any provider'));
230
- }
231
- else {
232
- for (const entry of auth.entries) {
233
- const active = entry.provider === auth.activeProvider ? chalk.green(' (active)') : '';
234
- const email = entry.email ? ` — ${entry.email}` : '';
235
- const expired = entry.expiresAt && entry.expiresAt < Date.now() ? chalk.red(' (expired)') : '';
236
- console.log(` ${entry.provider} (${entry.method})${email}${active}${expired}`);
237
- }
238
- }
239
- console.log('');
240
- });
241
- auth
242
- .command('apikey')
243
- .description('Set API key for a provider')
244
- .argument('<provider>', 'Provider name')
245
- .argument('<key>', 'API key')
246
- .action(async (provider, key) => {
247
- const auth = await loadAuth();
248
- auth.entries = auth.entries.filter((e) => e.provider !== provider);
249
- auth.entries.push({ provider, method: 'apikey', apiKey: key });
250
- auth.activeProvider = provider;
251
- await saveAuth(auth);
252
- console.log(chalk.green(` ✓ API key saved for ${provider}`));
253
- });
254
- }
255
- export { loadAuth, loadCliConfig, saveCliConfig, CONFIG_DIR, CONFIG_FILE };
@@ -1,2 +0,0 @@
1
- import { Command } from 'commander';
2
- export declare function configCommand(program: Command): void;
@@ -1,42 +0,0 @@
1
- import chalk from 'chalk';
2
- import { loadCliConfig, saveCliConfig } from '../utils/config.js';
3
- export function configCommand(program) {
4
- program
5
- .command('config')
6
- .description('View or edit CLI configuration')
7
- .option('--show', 'Show current configuration')
8
- .option('--set <key=value>', 'Set a config value')
9
- .action(async (opts) => {
10
- const config = await loadCliConfig();
11
- if (opts.show) {
12
- console.log(chalk.bold('\n Agent CLI Configuration'));
13
- console.log(chalk.dim(' ──────────────────────────'));
14
- console.log(chalk.yellow(' Providers:'), Object.keys(config.apiKeys).join(', ') || 'none');
15
- for (const [role, r] of Object.entries(config.roles)) {
16
- if (r)
17
- console.log(chalk.yellow(` ${role}:`), `${r.provider}/${r.model}`);
18
- }
19
- if (config.deliberation?.enabled) {
20
- console.log(chalk.yellow(' Deliberation:'), `enabled (${config.deliberation.max_rounds} rounds)`);
21
- }
22
- console.log('');
23
- return;
24
- }
25
- if (opts.set) {
26
- const [key, ...valParts] = opts.set.split('=');
27
- const value = valParts.join('=');
28
- const parts = key.split('.');
29
- let obj = config;
30
- for (let i = 0; i < parts.length - 1; i++) {
31
- if (!obj[parts[i]])
32
- obj[parts[i]] = {};
33
- obj = obj[parts[i]];
34
- }
35
- obj[parts[parts.length - 1]] = value;
36
- await saveCliConfig(config);
37
- console.log(chalk.green(` ✓ Set ${key} = ${value}`));
38
- return;
39
- }
40
- console.log('Use --show or --set key=value');
41
- });
42
- }
@@ -1,11 +0,0 @@
1
- import { TaskPlan } from '../types.js';
2
- export declare function orchestratorSystem(): string;
3
- export declare function orchestratorUser(taskId: string, task: string, context: string): string;
4
- export declare function implementorSystem(approved: string[], forbidden: string[]): string;
5
- export declare function implementorUser(taskId: string, plan: TaskPlan): string;
6
- export declare function reviewerSystem(): string;
7
- export declare function reviewerUser(taskId: string, plan: TaskPlan, progressSummary: string): string;
8
- export declare function proposerSystem(): string;
9
- export declare function proposerUser(task: string, question: string, prevRounds: string): string;
10
- export declare function criticSystem(): string;
11
- export declare function criticUser(task: string, question: string, proposal: string, prevCritiques: string): string;
@@ -1,126 +0,0 @@
1
- export function orchestratorSystem() {
2
- return `Sos el ORCHESTRATOR de un sistema multi-agente. Tu UNICO trabajo es planificar.
3
-
4
- REGLAS:
5
- - NO ejecutar codigo
6
- - NO crear archivos fuera de .agent/tasks/
7
- - NO instalar dependencias
8
- - NO modificar archivos existentes
9
- - Solo planificar y escribir JSON
10
-
11
- Tu output debe ser SOLO JSON valido, sin markdown, sin explicaciones.`;
12
- }
13
- export function orchestratorUser(taskId, task, context) {
14
- return `TAREA: ${task}
15
- ID: ${taskId}
16
-
17
- CONTEXTO:
18
- ${context}
19
-
20
- Responde SOLO con este JSON:
21
- {
22
- "plan": {
23
- "task_id": "${taskId}",
24
- "description": "${task}",
25
- "steps": [
26
- {"num": 1, "description": "paso detallado", "files": ["ruta/archivo"], "status": "pending"}
27
- ],
28
- "acceptance_criteria": ["criterio verificable 1", "criterio verificable 2"],
29
- "deliberation": {"needed": false, "question": ""}
30
- },
31
- "progress": {
32
- "task_id": "${taskId}",
33
- "created_at": "${new Date().toISOString()}",
34
- "steps": [],
35
- "status": "planned"
36
- }
37
- }
38
-
39
- Cada step debe ser atomico. Los criterios deben ser verificables.`;
40
- }
41
- export function implementorSystem(approved, forbidden) {
42
- return `Sos el IMPLEMENTOR. Tu UNICO trabajo es ejecutar el plan.
43
-
44
- REGLAS:
45
- - NO modificar plan.json
46
- - NO crear archivos no contemplados en el plan
47
- - Seguir el plan exactamente
48
- - Actualizar progress.json con timestamps
49
-
50
- APROBADOS: ${approved.join(', ')}
51
- PROHIBIDOS: ${forbidden.join(', ')}
52
-
53
- Tu output debe ser SOLO JSON valido con el progress actualizado.`;
54
- }
55
- export function implementorUser(taskId, plan) {
56
- return `EJECUTA ESTE PLAN:
57
- Task: ${taskId}
58
- ${JSON.stringify(plan, null, 2)}
59
-
60
- Por cada step completado, cambia status a "completed" y agrega completed_at.
61
- Al terminar todos los steps, status = "completed".
62
-
63
- Responde SOLO con el JSON de progress actualizado.`;
64
- }
65
- export function reviewerSystem() {
66
- return `Sos el REVIEWER. Tu UNICO trabajo es validar.
67
-
68
- REGLAS:
69
- - NO modificar codigo
70
- - NO implementar correcciones
71
- - Solo validar y reportar
72
-
73
- Tu output debe ser SOLO JSON valido.`;
74
- }
75
- export function reviewerUser(taskId, plan, progressSummary) {
76
- return `VALIDA ESTA TAREA:
77
- Task: ${taskId}
78
-
79
- PLAN:
80
- ${JSON.stringify({ description: plan.description, criteria: plan.acceptance_criteria }, null, 2)}
81
-
82
- PROGRESO:
83
- ${progressSummary}
84
-
85
- Responde SOLO con:
86
- {
87
- "result": {
88
- "task_id": "${taskId}",
89
- "criteria": [{"criterion": "...", "status": "PASS", "notes": "..."}],
90
- "verdict": "PASS",
91
- "explanation": "..."
92
- },
93
- "validation": {
94
- "status": "PASS",
95
- "reviewed_at": "${new Date().toISOString()}",
96
- "reviewer": "reviewer"
97
- }
98
- }
99
-
100
- Verdict = "PASS" si TODOS los criterios se cumplen, "FAIL" si alguno falla.`;
101
- }
102
- export function proposerSystem() {
103
- return `Sos el PROPOSER en un debate tecnico. Generas propuestas con justificacion tecnica.
104
- Responde SOLO con JSON valido.`;
105
- }
106
- export function proposerUser(task, question, prevRounds) {
107
- return `TAREA: ${task}
108
- PREGUNTA: ${question}
109
- ${prevRounds ? 'RONDAS ANTERIORES:\n' + prevRounds : 'Primera ronda.'}
110
-
111
- Responde SOLO con:
112
- {"proposal": "...", "justification": "...", "trade_offs": ["..."], "accept": false}`;
113
- }
114
- export function criticSystem() {
115
- return `Sos el CRITIC en un debate tecnico. Buscas debilidades, edge cases y errores.
116
- Responde SOLO con JSON valido.`;
117
- }
118
- export function criticUser(task, question, proposal, prevCritiques) {
119
- return `TAREA: ${task}
120
- PREGUNTA: ${question}
121
- PROPUESTA: ${proposal}
122
- ${prevCritiques ? 'CRITICAS ANTERIORES:\n' + prevCritiques : ''}
123
-
124
- Responde SOLO con:
125
- {"analysis": "...", "weaknesses": ["..."], "edge_cases": ["..."], "suggestions": ["..."], "verdict": "ACCEPT"}`;
126
- }
@@ -1,2 +0,0 @@
1
- import { RoleConfig, ChatResponse } from '../types.js';
2
- export declare function chat(role: RoleConfig, systemPrompt: string, userPrompt: string): Promise<ChatResponse>;
@@ -1,120 +0,0 @@
1
- import OpenAI from 'openai';
2
- import { Anthropic } from '@anthropic-ai/sdk';
3
- import { GoogleGenerativeAI } from '@google/generative-ai';
4
- import { loadAuth } from '../commands/auth.js';
5
- function detectProvider(model) {
6
- const m = model.toLowerCase();
7
- if (m.startsWith('gpt') || m.startsWith('o1') || m.startsWith('o3') || m.startsWith('o4'))
8
- return 'openai';
9
- if (m.startsWith('claude'))
10
- return 'anthropic';
11
- if (m.startsWith('gemini'))
12
- return 'google';
13
- if (m.includes('/'))
14
- return 'opencode';
15
- return 'openai';
16
- }
17
- async function getAuthToken(provider) {
18
- // 1. Check env vars
19
- const envKey = process.env[`${provider.toUpperCase()}_API_KEY`];
20
- if (envKey)
21
- return envKey;
22
- // 2. Check OAuth auth store
23
- try {
24
- const auth = await loadAuth();
25
- const entry = auth.entries.find((e) => e.provider === provider) ||
26
- auth.entries.find((e) => e.provider === auth.activeProvider);
27
- if (entry?.accessToken)
28
- return entry.accessToken;
29
- if (entry?.apiKey)
30
- return entry.apiKey;
31
- }
32
- catch { }
33
- return undefined;
34
- }
35
- async function buildClient(role) {
36
- const provider = role.provider || detectProvider(role.model);
37
- const token = await getAuthToken(provider);
38
- switch (provider) {
39
- case 'openai':
40
- return { type: 'openai', client: new OpenAI({ apiKey: token }) };
41
- case 'anthropic':
42
- return { type: 'anthropic', client: new Anthropic({ apiKey: token }) };
43
- case 'google':
44
- return { type: 'google', client: new GoogleGenerativeAI(token || '') };
45
- case 'opencode': {
46
- // Opencode uses an OpenAI-compatible API
47
- const baseURL = process.env.OPENCODE_BASE_URL || 'https://api.opencode.ai/v1';
48
- return { type: 'opencode', client: new OpenAI({ apiKey: token || '', baseURL }) };
49
- }
50
- default:
51
- throw new Error(`Unknown provider: ${provider}`);
52
- }
53
- }
54
- export async function chat(role, systemPrompt, userPrompt) {
55
- const client = await buildClient(role);
56
- switch (client.type) {
57
- case 'openai':
58
- case 'opencode':
59
- return chatOpenai(client.client, role.model, systemPrompt, userPrompt);
60
- case 'anthropic':
61
- return chatAnthropic(client.client, role.model, systemPrompt, userPrompt);
62
- case 'google':
63
- return chatGoogle(client.client, role.model, systemPrompt, userPrompt);
64
- }
65
- }
66
- async function chatOpenai(client, model, system, user) {
67
- const res = await client.chat.completions.create({
68
- model,
69
- messages: [
70
- { role: 'system', content: system },
71
- { role: 'user', content: user },
72
- ],
73
- });
74
- const content = res.choices[0]?.message?.content || '';
75
- const usage = res.usage;
76
- return {
77
- content,
78
- usage: {
79
- input_tokens: usage?.prompt_tokens || 0,
80
- output_tokens: usage?.completion_tokens || 0,
81
- cache_read_input_tokens: 0,
82
- },
83
- };
84
- }
85
- async function chatAnthropic(client, model, system, user) {
86
- const res = await client.messages.create({
87
- model,
88
- system,
89
- max_tokens: 8192,
90
- messages: [{ role: 'user', content: user }],
91
- });
92
- const content = res.content
93
- .filter((b) => b.type === 'text')
94
- .map((b) => b.text)
95
- .join('\n');
96
- return {
97
- content,
98
- usage: {
99
- input_tokens: res.usage?.input_tokens || 0,
100
- output_tokens: res.usage?.output_tokens || 0,
101
- cache_read_input_tokens: res.usage?.cache_read_input_tokens || 0,
102
- },
103
- };
104
- }
105
- async function chatGoogle(client, model, system, user) {
106
- const genModel = client.getGenerativeModel({ model });
107
- const result = await genModel.generateContent({
108
- contents: [{ role: 'user', parts: [{ text: `${system}\n\n---\n\n${user}` }] }],
109
- });
110
- const text = result.response.text();
111
- const usageMetadata = result.response.usageMetadata;
112
- return {
113
- content: text,
114
- usage: {
115
- input_tokens: usageMetadata?.promptTokenCount || 0,
116
- output_tokens: usageMetadata?.candidatesTokenCount || 0,
117
- cache_read_input_tokens: 0,
118
- },
119
- };
120
- }