agent-mp 0.4.15 → 0.5.1

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.
@@ -7,7 +7,7 @@ import chalk from 'chalk';
7
7
  import { execSync } from 'child_process';
8
8
  import { CLI_REGISTRY } from '../types.js';
9
9
  import { writeJson, ensureDir, readJson, listDir, fileExists } from '../utils/fs.js';
10
- import { loadAuth, saveAuth, loadCliConfig, saveCliConfig, loadProjectConfig } from '../utils/config.js';
10
+ import { loadAuth, saveAuth, loadCliConfig, saveCliConfig, loadProjectConfig, resolveProjectDir } from '../utils/config.js';
11
11
  import { log } from '../utils/logger.js';
12
12
  import { AgentEngine, ExitError } from '../core/engine.js';
13
13
  import { qwenLogin, qwenAuthStatus, QWEN_AGENT_HOME, fetchQwenModels } from '../utils/qwen-auth.js';
@@ -594,7 +594,7 @@ async function cmdStatus(fi) {
594
594
  roleRows.push({ key: 'global fallback', value: `${cliConfig.fallback_global.cli}/${cliConfig.fallback_global.model}` });
595
595
  }
596
596
  sections.push({ title: 'Roles — full cmd', rows: roleRows });
597
- const dir = process.cwd();
597
+ const dir = await resolveProjectDir(process.cwd());
598
598
  try {
599
599
  const config = await loadProjectConfig(dir);
600
600
  const projectRows = [
@@ -711,7 +711,7 @@ function cmdHelp(fi) {
711
711
  fi.println(renderSectionBox('Commands', commands));
712
712
  }
713
713
  async function cmdTasks(fi) {
714
- const dir = process.cwd();
714
+ const dir = await resolveProjectDir(process.cwd());
715
715
  const tasksDir = path.join(dir, '.agent', 'tasks');
716
716
  const tasks = await listDir(tasksDir);
717
717
  if (tasks.length === 0) {
@@ -858,13 +858,15 @@ export async function runRepl(resumeSession) {
858
858
  const { activeCli } = init;
859
859
  const rl = init.rl;
860
860
  const session = resumeSession ?? newSession(process.cwd());
861
+ // Resolve actual project root (may be deeper than cwd)
862
+ const projectDir = await resolveProjectDir(process.cwd());
861
863
  // Show project status and FORCE role configuration if missing
862
864
  let hasRoles = false;
863
865
  let welcomeRoles;
864
- let welcomeProject = path.basename(process.cwd());
866
+ let welcomeProject = path.basename(projectDir);
865
867
  let welcomeStack = 'generic';
866
868
  try {
867
- const config = await loadProjectConfig(process.cwd());
869
+ const config = await loadProjectConfig(projectDir);
868
870
  welcomeProject = config.project;
869
871
  welcomeStack = config.stack;
870
872
  if (config.roles?.orchestrator?.cli && config.roles?.implementor?.cli && config.roles?.reviewer?.cli) {
@@ -915,8 +917,7 @@ export async function runRepl(resumeSession) {
915
917
  process.stdout.write(renderHelpHint() + '\n\n');
916
918
  // Print coordinator header BEFORE fi.setup() so no blank lines appear between them
917
919
  try {
918
- const dir = process.cwd();
919
- const config = await loadProjectConfig(dir);
920
+ const config = await loadProjectConfig(projectDir);
920
921
  if (config.roles?.orchestrator?.cli) {
921
922
  process.stdout.write('\n');
922
923
  process.stdout.write(chalk.bold.cyan('═'.repeat(46)) + '\n');
@@ -990,7 +991,7 @@ export async function runRepl(resumeSession) {
990
991
  await cmdStatus(fi);
991
992
  break;
992
993
  case 'run': {
993
- const dir = process.cwd();
994
+ const dir = await resolveProjectDir(process.cwd());
994
995
  const config = await loadProjectConfig(dir);
995
996
  if (args[0] === 'orch' || args[0] === 'orchestrator') {
996
997
  const task = args.slice(1).join(' ');
@@ -1027,7 +1028,7 @@ export async function runRepl(resumeSession) {
1027
1028
  }
1028
1029
  case 'explorer':
1029
1030
  case 'exp': {
1030
- const dir = process.cwd();
1031
+ const dir = await resolveProjectDir(process.cwd());
1031
1032
  const config = await loadProjectConfig(dir);
1032
1033
  const task = args.join(' ');
1033
1034
  const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
@@ -1099,7 +1100,7 @@ export async function runRepl(resumeSession) {
1099
1100
  session.messages.push({ role: 'user', content: trimmed, ts: new Date().toISOString() });
1100
1101
  await saveSession(session);
1101
1102
  try {
1102
- const dir = process.cwd();
1103
+ const dir = await resolveProjectDir(process.cwd());
1103
1104
  const config = await loadProjectConfig(dir);
1104
1105
  if (!config.roles?.orchestrator?.cli) {
1105
1106
  fi.println(chalk.red(' No roles configured. Run /config-multi first.'));
@@ -1136,25 +1137,24 @@ export async function runRepl(resumeSession) {
1136
1137
  * Prompt estricto: cada rol solo hace lo que debe, sin extras.
1137
1138
  */
1138
1139
  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;
1144
- const dir = process.cwd();
1140
+ const { PKG_NAME, getConfigDir } = await import('../utils/config.js');
1141
+ // Detect if running a native role binary directly (agent-orch, agent-impl, etc.)
1142
+ // In that case, skip REPL and run headless — no coordinator needed
1143
+ const ROLE_BINS = new Set(['agent-orch', 'agent-impl', 'agent-rev', 'agent-explorer']);
1144
+ const isNativeBin = ROLE_BINS.has(PKG_NAME);
1145
+ // Resolve the actual project root (may be deeper than cwd)
1146
+ const dir = await resolveProjectDir(process.cwd());
1145
1147
  let config;
1146
1148
  try {
1147
1149
  config = await loadProjectConfig(dir);
1148
1150
  }
1149
1151
  catch {
1150
1152
  console.log(chalk.red(' No project config found. Run /setup first.'));
1151
- rl.close();
1152
1153
  process.exit(1);
1153
1154
  }
1154
1155
  // Validate roles are configured
1155
1156
  if (!config.roles?.orchestrator?.cli || !config.roles?.implementor?.cli || !config.roles?.reviewer?.cli) {
1156
1157
  console.log(chalk.red(' Roles not configured. Run setup first.'));
1157
- rl.close();
1158
1158
  process.exit(1);
1159
1159
  }
1160
1160
  // Override model if passed via --model flag
@@ -1170,7 +1170,60 @@ export async function runRole(role, arg, model) {
1170
1170
  r.cmd = r.cmd.replace(/(-m|--model)\s+\S+/, `$1 ${model}`);
1171
1171
  }
1172
1172
  }
1173
- console.log(chalk.bold.cyan(`\n Agent-mp — Rol: ${role.toUpperCase()}${model ? ` (${model})` : ''}\n`));
1173
+ console.log(chalk.bold.cyan(`\n ${PKG_NAME} — Rol: ${role.toUpperCase()}${model ? ` (${model})` : ''}\n`));
1174
+ if (isNativeBin) {
1175
+ // Headless mode: create a dummy RL (no stdin), run engine directly
1176
+ const { Readable } = await import('stream');
1177
+ const emptyStream = new Readable({ read() { this.push(null); } });
1178
+ const dummyRl = readline.createInterface({ input: emptyStream, output: process.stdout });
1179
+ const engine = new AgentEngine(config, dir, '', dummyRl);
1180
+ try {
1181
+ switch (role.toLowerCase()) {
1182
+ case 'orchestrator':
1183
+ case 'orch': {
1184
+ const result = await engine.runOrchestrator(arg);
1185
+ log.ok(`Task ID: ${result.taskId}`);
1186
+ break;
1187
+ }
1188
+ case 'implementor':
1189
+ case 'impl': {
1190
+ const taskDir = path.join(dir, '.agent', 'tasks', arg);
1191
+ const plan = await readJson(path.join(taskDir, 'plan.json'));
1192
+ await engine.runImplementor(arg, plan);
1193
+ break;
1194
+ }
1195
+ case 'reviewer':
1196
+ case 'rev': {
1197
+ const taskDir = path.join(dir, '.agent', 'tasks', arg);
1198
+ const plan = await readJson(path.join(taskDir, 'plan.json'));
1199
+ const progress = await readJson(path.join(taskDir, 'progress.json'));
1200
+ await engine.runReviewer(arg, plan, progress);
1201
+ break;
1202
+ }
1203
+ case 'explorer':
1204
+ case 'exp': {
1205
+ const result = await engine.runExplorer(arg);
1206
+ log.ok(`Explorer: ${result.substring(0, 100)}...`);
1207
+ break;
1208
+ }
1209
+ default:
1210
+ console.log(chalk.red(` Unknown role: ${role}. Use: orchestrator, implementor, reviewer, explorer`));
1211
+ process.exit(1);
1212
+ }
1213
+ }
1214
+ catch (err) {
1215
+ console.log(chalk.red(` Error: ${err.message}`));
1216
+ process.exit(1);
1217
+ }
1218
+ dummyRl.close();
1219
+ return;
1220
+ }
1221
+ // REPL mode: needs coordinator
1222
+ const init = await initCoordinator();
1223
+ if (!init)
1224
+ process.exit(1);
1225
+ const { coordinatorCmd } = init;
1226
+ const rl = init.rl;
1174
1227
  const engine = new AgentEngine(config, dir, coordinatorCmd, rl);
1175
1228
  try {
1176
1229
  switch (role.toLowerCase()) {
@@ -1195,6 +1248,12 @@ export async function runRole(role, arg, model) {
1195
1248
  await engine.runReviewer(arg, plan, progress);
1196
1249
  break;
1197
1250
  }
1251
+ case 'explorer':
1252
+ case 'exp': {
1253
+ const result = await engine.runExplorer(arg);
1254
+ log.ok(`Explorer: ${result.substring(0, 100)}...`);
1255
+ break;
1256
+ }
1198
1257
  case 'coordinator':
1199
1258
  case 'coord': {
1200
1259
  console.log(chalk.yellow(' Coordinator mode requires interactive REPL.'));
@@ -1202,7 +1261,7 @@ export async function runRole(role, arg, model) {
1202
1261
  process.exit(1);
1203
1262
  }
1204
1263
  default:
1205
- console.log(chalk.red(` Unknown role: ${role}. Use: orchestrator, implementor, reviewer`));
1264
+ console.log(chalk.red(` Unknown role: ${role}. Use: orchestrator, implementor, reviewer, explorer`));
1206
1265
  process.exit(1);
1207
1266
  }
1208
1267
  }
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>"`));
@@ -1,4 +1,11 @@
1
1
  import { AgentConfig } from '../types.js';
2
+ /**
3
+ * Resolve the actual project root starting from a given directory.
4
+ * If .agent/config.json exists in the start dir, scan subdirectories
5
+ * for a deeper project root (indicated by common project files).
6
+ * Returns the deepest valid project root found, or the original dir.
7
+ */
8
+ export declare function resolveProjectDir(startDir: string): Promise<string>;
2
9
  export declare const PKG_NAME: string;
3
10
  export declare const AGENT_HOME: string;
4
11
  export declare const AUTH_FILE: string;
@@ -3,6 +3,56 @@ import * as fsSync from 'fs';
3
3
  import * as path from 'path';
4
4
  import * as os from 'os';
5
5
  const KNOWN_PKGS = ['agent-mp', 'agent-orch', 'agent-impl', 'agent-rev', 'agent-explorer'];
6
+ // Files/dirs that indicate a directory is the real project root
7
+ const PROJECT_INDICATORS = [
8
+ 'package.json', 'pyproject.toml', 'requirements.txt', 'setup.py', 'setup.cfg',
9
+ 'Cargo.toml', 'go.mod', 'Gemfile', 'pom.xml', 'build.gradle', 'build.gradle.kts',
10
+ 'CMakeLists.txt', 'Makefile', 'composer.json', 'mix.exs', 'pubspec.yaml',
11
+ 'src', 'app', 'lib', 'main.py', 'app.py', 'index.js', 'index.ts',
12
+ 'main.go', 'main.rs', 'lib.rs', 'app.js', 'app.ts',
13
+ ];
14
+ /**
15
+ * Resolve the actual project root starting from a given directory.
16
+ * If .agent/config.json exists in the start dir, scan subdirectories
17
+ * for a deeper project root (indicated by common project files).
18
+ * Returns the deepest valid project root found, or the original dir.
19
+ */
20
+ export async function resolveProjectDir(startDir) {
21
+ const hasAgentConfig = await fileExists(path.join(startDir, '.agent', 'config.json'));
22
+ if (!hasAgentConfig)
23
+ return startDir;
24
+ // Scan immediate subdirectories for a deeper project root
25
+ let bestDir = startDir;
26
+ let bestScore = 0;
27
+ try {
28
+ const entries = await fs.readdir(startDir, { withFileTypes: true });
29
+ for (const entry of entries) {
30
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules')
31
+ continue;
32
+ const subDir = path.join(startDir, entry.name);
33
+ let score = 0;
34
+ for (const indicator of PROJECT_INDICATORS) {
35
+ if (await fileExists(path.join(subDir, indicator)))
36
+ score++;
37
+ }
38
+ if (score > bestScore) {
39
+ bestScore = score;
40
+ bestDir = subDir;
41
+ }
42
+ }
43
+ }
44
+ catch { /* ignore */ }
45
+ return bestDir;
46
+ }
47
+ async function fileExists(p) {
48
+ try {
49
+ await fs.stat(p);
50
+ return true;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
6
56
  function resolvePackageName() {
7
57
  // 1. Read package.json (reliable for both global installs and dev mode)
8
58
  const __filename = new URL(import.meta.url).pathname;
@@ -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.15",
3
+ "version": "0.5.1",
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
- }