agent-rev 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/dist/api/qwen.d.ts +12 -0
- package/dist/api/qwen.js +150 -0
- package/dist/commands/auth.d.ts +31 -0
- package/dist/commands/auth.js +255 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +42 -0
- package/dist/commands/models.d.ts +2 -0
- package/dist/commands/models.js +27 -0
- package/dist/commands/repl.d.ts +29 -0
- package/dist/commands/repl.js +1167 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +52 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +353 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +46 -0
- package/dist/core/engine.d.ts +36 -0
- package/dist/core/engine.js +905 -0
- package/dist/core/prompts.d.ts +11 -0
- package/dist/core/prompts.js +126 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +171 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +120 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.js +58 -0
- package/dist/ui/input.d.ts +24 -0
- package/dist/ui/input.js +244 -0
- package/dist/ui/theme.d.ts +99 -0
- package/dist/ui/theme.js +307 -0
- package/dist/utils/config.d.ts +38 -0
- package/dist/utils/config.js +40 -0
- package/dist/utils/fs.d.ts +7 -0
- package/dist/utils/fs.js +37 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.js +46 -0
- package/dist/utils/qwen-auth.d.ts +12 -0
- package/dist/utils/qwen-auth.js +250 -0
- package/dist/utils/sessions.d.ts +17 -0
- package/dist/utils/sessions.js +46 -0
- package/package.json +44 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { AgentConfig } from '../types.js';
|
|
2
|
+
export declare const AGENT_HOME: string;
|
|
3
|
+
export declare const AUTH_FILE: string;
|
|
4
|
+
export declare const CONFIG_FILE: string;
|
|
5
|
+
export interface CliConfig {
|
|
6
|
+
coordinatorProvider?: string;
|
|
7
|
+
coordinatorModel?: string;
|
|
8
|
+
roles: Record<string, {
|
|
9
|
+
cli: string;
|
|
10
|
+
model: string;
|
|
11
|
+
fallbackCli?: string;
|
|
12
|
+
fallbackModel?: string;
|
|
13
|
+
}>;
|
|
14
|
+
fallback_global?: {
|
|
15
|
+
cli: string;
|
|
16
|
+
model: string;
|
|
17
|
+
};
|
|
18
|
+
deliberation: {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
max_rounds: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface AuthStore {
|
|
24
|
+
activeProvider?: string;
|
|
25
|
+
entries: Array<{
|
|
26
|
+
provider: string;
|
|
27
|
+
method: 'oauth' | 'apikey';
|
|
28
|
+
accessToken?: string;
|
|
29
|
+
apiKey?: string;
|
|
30
|
+
email?: string;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
export declare function loadAuth(): Promise<AuthStore>;
|
|
34
|
+
export declare function saveAuth(auth: AuthStore): Promise<void>;
|
|
35
|
+
export declare function loadCliConfig(): Promise<CliConfig>;
|
|
36
|
+
export declare function saveCliConfig(cfg: CliConfig): Promise<void>;
|
|
37
|
+
export declare function loadProjectConfig(projectDir: string): Promise<AgentConfig>;
|
|
38
|
+
export declare function getConfigDir(): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
export const AGENT_HOME = path.join(os.homedir(), '.agent-cli');
|
|
5
|
+
export const AUTH_FILE = path.join(AGENT_HOME, 'auth.json');
|
|
6
|
+
export const CONFIG_FILE = path.join(AGENT_HOME, 'config.json');
|
|
7
|
+
export async function loadAuth() {
|
|
8
|
+
try {
|
|
9
|
+
const content = await fs.readFile(AUTH_FILE, 'utf-8');
|
|
10
|
+
return JSON.parse(content);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return { entries: [] };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function saveAuth(auth) {
|
|
17
|
+
await fs.mkdir(AGENT_HOME, { recursive: true });
|
|
18
|
+
await fs.writeFile(AUTH_FILE, JSON.stringify(auth, null, 2) + '\n', 'utf-8');
|
|
19
|
+
}
|
|
20
|
+
export async function loadCliConfig() {
|
|
21
|
+
try {
|
|
22
|
+
const content = await fs.readFile(CONFIG_FILE, 'utf-8');
|
|
23
|
+
return JSON.parse(content);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { roles: {}, deliberation: { enabled: false, max_rounds: 4 } };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function saveCliConfig(cfg) {
|
|
30
|
+
await fs.mkdir(AGENT_HOME, { recursive: true });
|
|
31
|
+
await fs.writeFile(CONFIG_FILE, JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
export async function loadProjectConfig(projectDir) {
|
|
34
|
+
const configPath = path.join(projectDir, '.agent', 'config.json');
|
|
35
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
36
|
+
return JSON.parse(content);
|
|
37
|
+
}
|
|
38
|
+
export function getConfigDir() {
|
|
39
|
+
return AGENT_HOME;
|
|
40
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function ensureDir(dir: string): Promise<void>;
|
|
2
|
+
export declare function writeJson(filePath: string, data: unknown): Promise<void>;
|
|
3
|
+
export declare function readJson<T>(filePath: string): Promise<T>;
|
|
4
|
+
export declare function fileExists(filePath: string): Promise<boolean>;
|
|
5
|
+
export declare function writeFile(filePath: string, content: string): Promise<void>;
|
|
6
|
+
export declare function readFile(filePath: string): Promise<string>;
|
|
7
|
+
export declare function listDir(dir: string): Promise<string[]>;
|
package/dist/utils/fs.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
export async function ensureDir(dir) {
|
|
4
|
+
await fs.mkdir(dir, { recursive: true });
|
|
5
|
+
}
|
|
6
|
+
export async function writeJson(filePath, data) {
|
|
7
|
+
await ensureDir(path.dirname(filePath));
|
|
8
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
9
|
+
}
|
|
10
|
+
export async function readJson(filePath) {
|
|
11
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
12
|
+
return JSON.parse(content);
|
|
13
|
+
}
|
|
14
|
+
export async function fileExists(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
await fs.access(filePath);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function writeFile(filePath, content) {
|
|
24
|
+
await ensureDir(path.dirname(filePath));
|
|
25
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
export async function readFile(filePath) {
|
|
28
|
+
return fs.readFile(filePath, 'utf-8');
|
|
29
|
+
}
|
|
30
|
+
export async function listDir(dir) {
|
|
31
|
+
try {
|
|
32
|
+
return await fs.readdir(dir);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const log: {
|
|
2
|
+
header: (text: string) => void;
|
|
3
|
+
section: (text: string) => void;
|
|
4
|
+
ok: (text: string) => void;
|
|
5
|
+
error: (text: string) => void;
|
|
6
|
+
warn: (text: string) => void;
|
|
7
|
+
info: (text: string) => void;
|
|
8
|
+
phase: (num: number, name: string, cli: string, model: string) => void;
|
|
9
|
+
verdict: (verdict: string) => void;
|
|
10
|
+
tokens: (input: number, output: number, cached: number) => void;
|
|
11
|
+
divider: () => void;
|
|
12
|
+
cliList: (name: string, path: string) => void;
|
|
13
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export const log = {
|
|
3
|
+
header: (text) => {
|
|
4
|
+
console.log('');
|
|
5
|
+
console.log(chalk.bold.cyan('\u2550'.repeat(46)));
|
|
6
|
+
console.log(chalk.bold.cyan(` ${text}`));
|
|
7
|
+
console.log(chalk.bold.cyan('\u2550'.repeat(46)));
|
|
8
|
+
},
|
|
9
|
+
section: (text) => {
|
|
10
|
+
console.log('');
|
|
11
|
+
console.log(chalk.bold.yellow(` \u2500\u2500 ${text} \u2500\u2500`));
|
|
12
|
+
},
|
|
13
|
+
ok: (text) => {
|
|
14
|
+
console.log(chalk.green(` \u2713 ${text}`));
|
|
15
|
+
},
|
|
16
|
+
error: (text) => {
|
|
17
|
+
console.log(chalk.red(` \u2717 ${text}`));
|
|
18
|
+
},
|
|
19
|
+
warn: (text) => {
|
|
20
|
+
console.log(chalk.yellow(` ! ${text}`));
|
|
21
|
+
},
|
|
22
|
+
info: (text) => {
|
|
23
|
+
console.log(chalk.blue(` \u2192 ${text}`));
|
|
24
|
+
},
|
|
25
|
+
phase: (num, name, cli, model) => {
|
|
26
|
+
console.log('');
|
|
27
|
+
console.log(chalk.bold.bgBlue.white(` FASE ${num}: ${name.toUpperCase()} (${cli}/${model}) `));
|
|
28
|
+
},
|
|
29
|
+
verdict: (verdict) => {
|
|
30
|
+
if (verdict === 'PASS') {
|
|
31
|
+
console.log(chalk.bold.green(` \u2605 VERDICTO: ${verdict} \u2605`));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log(chalk.bold.red(` \u2605 VERDICTO: ${verdict} \u2605`));
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
tokens: (input, output, cached) => {
|
|
38
|
+
console.log(chalk.dim(` Input: ${input.toLocaleString()} | Output: ${output.toLocaleString()} | Cached: ${cached.toLocaleString()}`));
|
|
39
|
+
},
|
|
40
|
+
divider: () => {
|
|
41
|
+
console.log(chalk.dim(' ' + '\u2500'.repeat(42)));
|
|
42
|
+
},
|
|
43
|
+
cliList: (name, path) => {
|
|
44
|
+
console.log(chalk.green(` [OK] ${name} \u2192 ${path}`));
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Exported as const for backward compat */
|
|
2
|
+
export declare const QWEN_AGENT_HOME: string;
|
|
3
|
+
/** Dynamic getter (recommended for runtime changes) */
|
|
4
|
+
export declare function getQwenHome(): string;
|
|
5
|
+
export declare function getTokenPath(): Promise<string>;
|
|
6
|
+
export declare function qwenLogin(): Promise<boolean>;
|
|
7
|
+
export declare function qwenAuthStatus(): Promise<{
|
|
8
|
+
authenticated: boolean;
|
|
9
|
+
email?: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function getQwenAccessToken(): Promise<string | null>;
|
|
12
|
+
export declare function callQwenAPI(prompt: string, model?: string): Promise<string>;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
import open from 'open';
|
|
5
|
+
import { AGENT_HOME } from './config.js';
|
|
6
|
+
const QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai';
|
|
7
|
+
const QWEN_OAUTH_DEVICE_CODE_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/device/code`;
|
|
8
|
+
const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`;
|
|
9
|
+
const QWEN_OAUTH_CLIENT_ID = 'f0304373b74a44d2b584a3fb70ca9e56';
|
|
10
|
+
const QWEN_OAUTH_SCOPE = 'openid profile email model.completion';
|
|
11
|
+
/** Get Qwen credentials dir inside agent's home */
|
|
12
|
+
function getQwenAgentHome() {
|
|
13
|
+
return AGENT_HOME;
|
|
14
|
+
}
|
|
15
|
+
/** Exported as const for backward compat */
|
|
16
|
+
export const QWEN_AGENT_HOME = AGENT_HOME;
|
|
17
|
+
/** Dynamic getter (recommended for runtime changes) */
|
|
18
|
+
export function getQwenHome() {
|
|
19
|
+
return AGENT_HOME;
|
|
20
|
+
}
|
|
21
|
+
async function getQwenDir() {
|
|
22
|
+
const dir = getQwenAgentHome();
|
|
23
|
+
await fs.mkdir(dir, { recursive: true });
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
export async function getTokenPath() {
|
|
27
|
+
const qwenDir = await getQwenDir();
|
|
28
|
+
return path.join(qwenDir, 'oauth_creds.json');
|
|
29
|
+
}
|
|
30
|
+
async function loadToken() {
|
|
31
|
+
try {
|
|
32
|
+
const tokenPath = await getTokenPath();
|
|
33
|
+
const content = await fs.readFile(tokenPath, 'utf-8');
|
|
34
|
+
const raw = JSON.parse(content);
|
|
35
|
+
// Normalize: qwen CLI saves snake_case, we save camelCase
|
|
36
|
+
const token = {
|
|
37
|
+
accessToken: raw.accessToken || raw.access_token || '',
|
|
38
|
+
refreshToken: raw.refreshToken || raw.refresh_token || '',
|
|
39
|
+
expiresAt: raw.expiresAt || raw.expiry_date || 0,
|
|
40
|
+
idToken: raw.idToken || raw.id_token,
|
|
41
|
+
resourceUrl: raw.resourceUrl || raw.resource_url,
|
|
42
|
+
};
|
|
43
|
+
if (!token.accessToken)
|
|
44
|
+
return null;
|
|
45
|
+
if (token.expiresAt > Date.now()) {
|
|
46
|
+
return token;
|
|
47
|
+
}
|
|
48
|
+
// Token expirado — intentar refresh
|
|
49
|
+
if (token.refreshToken) {
|
|
50
|
+
return refreshAccessToken(token.refreshToken);
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function saveToken(token) {
|
|
59
|
+
const tokenPath = await getTokenPath();
|
|
60
|
+
await fs.writeFile(tokenPath, JSON.stringify(token, null, 2), 'utf-8');
|
|
61
|
+
}
|
|
62
|
+
async function refreshAccessToken(refreshToken) {
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
67
|
+
body: new URLSearchParams({
|
|
68
|
+
grant_type: 'refresh_token',
|
|
69
|
+
refresh_token: refreshToken,
|
|
70
|
+
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
75
|
+
}
|
|
76
|
+
const data = await response.json();
|
|
77
|
+
const token = {
|
|
78
|
+
accessToken: data.access_token,
|
|
79
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
80
|
+
expiresAt: Date.now() + (data.expires_in || 3600) * 1000,
|
|
81
|
+
idToken: data.id_token,
|
|
82
|
+
resourceUrl: data.resource_url,
|
|
83
|
+
};
|
|
84
|
+
await saveToken(token);
|
|
85
|
+
return token;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.error('Error refreshing token:', error);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Generate PKCE code verifier and challenge
|
|
93
|
+
function generatePKCE() {
|
|
94
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
95
|
+
const codeChallenge = crypto
|
|
96
|
+
.createHash('sha256')
|
|
97
|
+
.update(codeVerifier)
|
|
98
|
+
.digest('base64url');
|
|
99
|
+
return { codeVerifier, codeChallenge };
|
|
100
|
+
}
|
|
101
|
+
export async function qwenLogin() {
|
|
102
|
+
try {
|
|
103
|
+
// Step 1: Generate PKCE
|
|
104
|
+
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
105
|
+
// Step 2: Get device code with PKCE challenge
|
|
106
|
+
const deviceCodeResponse = await fetch(QWEN_OAUTH_DEVICE_CODE_ENDPOINT, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: {
|
|
109
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
110
|
+
'Accept': 'application/json',
|
|
111
|
+
'User-Agent': 'Qwen-Agent-CLI/1.0',
|
|
112
|
+
'x-request-id': crypto.randomUUID(),
|
|
113
|
+
},
|
|
114
|
+
body: new URLSearchParams({
|
|
115
|
+
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
116
|
+
scope: QWEN_OAUTH_SCOPE,
|
|
117
|
+
code_challenge: codeChallenge,
|
|
118
|
+
code_challenge_method: 'S256',
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
if (!deviceCodeResponse.ok) {
|
|
122
|
+
const errorText = await deviceCodeResponse.text();
|
|
123
|
+
throw new Error(`Failed to get device code: ${deviceCodeResponse.status} - ${errorText}`);
|
|
124
|
+
}
|
|
125
|
+
const deviceCodeData = await deviceCodeResponse.json();
|
|
126
|
+
const { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval } = deviceCodeData;
|
|
127
|
+
// Step 3: Show instructions to user
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log('╔═══════════════════════════════════════════════════════════╗');
|
|
130
|
+
console.log('║ Autenticación con Qwen OAuth ║');
|
|
131
|
+
console.log('╚═══════════════════════════════════════════════════════════╝');
|
|
132
|
+
console.log('');
|
|
133
|
+
console.log(` 1. Abrí esta URL en tu navegador:`);
|
|
134
|
+
console.log(` ${verification_uri_complete || verification_uri}`);
|
|
135
|
+
console.log('');
|
|
136
|
+
console.log(` 2. Ingresá el código:`);
|
|
137
|
+
console.log(` ${user_code}`);
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log(' El código expira en', Math.floor(expires_in / 60), 'minutos.');
|
|
140
|
+
console.log('');
|
|
141
|
+
// Step 4: Open browser automatically with complete URL
|
|
142
|
+
const urlToOpen = verification_uri_complete || verification_uri;
|
|
143
|
+
console.log(' ✳️ IMPORTANTE: Si tu navegador ya tiene sesión con otra cuenta,');
|
|
144
|
+
console.log(' copiá la URL y abrila en modo incógnito/privado.');
|
|
145
|
+
console.log('');
|
|
146
|
+
await open(urlToOpen);
|
|
147
|
+
console.log(' ✓ Navegador abierto');
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log(' Esperando autorización...');
|
|
150
|
+
// Step 5: Poll for token with PKCE verifier
|
|
151
|
+
const token = await pollForToken(device_code, codeVerifier, interval || 5, expires_in);
|
|
152
|
+
if (token) {
|
|
153
|
+
await saveToken(token);
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log(' ✓ ¡Autenticación exitosa!');
|
|
156
|
+
console.log(' Token guardado en:', await getTokenPath());
|
|
157
|
+
console.log('');
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
console.error(' ✗ Error en autenticación:', error.message);
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log(' Alternativa: usá el CLI de Qwen directamente:');
|
|
166
|
+
console.log(' qwen auth qwen-oauth');
|
|
167
|
+
console.log('');
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function pollForToken(deviceCode, codeVerifier, interval, expiresIn) {
|
|
172
|
+
const maxAttempts = Math.floor(expiresIn / interval);
|
|
173
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
174
|
+
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
|
175
|
+
try {
|
|
176
|
+
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
179
|
+
body: new URLSearchParams({
|
|
180
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
181
|
+
device_code: deviceCode,
|
|
182
|
+
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
183
|
+
code_verifier: codeVerifier,
|
|
184
|
+
}),
|
|
185
|
+
});
|
|
186
|
+
const data = await response.json();
|
|
187
|
+
if (response.ok && data.access_token) {
|
|
188
|
+
return {
|
|
189
|
+
accessToken: data.access_token,
|
|
190
|
+
refreshToken: data.refresh_token || '',
|
|
191
|
+
expiresAt: Date.now() + (data.expires_in || 3600) * 1000,
|
|
192
|
+
idToken: data.id_token,
|
|
193
|
+
resourceUrl: data.resource_url,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// authorization_pending - keep polling
|
|
197
|
+
if (data.error === 'authorization_pending') {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
// Other errors - stop polling
|
|
201
|
+
if (data.error === 'access_denied' || data.error === 'expired_token') {
|
|
202
|
+
console.log(' ✗ Autorización denegada o código expirado');
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
// Continue polling on network errors
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
console.log(' ✗ Tiempo de espera agotado');
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
export async function qwenAuthStatus() {
|
|
214
|
+
const token = await loadToken();
|
|
215
|
+
if (!token)
|
|
216
|
+
return { authenticated: false };
|
|
217
|
+
return { authenticated: true };
|
|
218
|
+
}
|
|
219
|
+
export async function getQwenAccessToken() {
|
|
220
|
+
const token = await loadToken();
|
|
221
|
+
return token?.accessToken || null;
|
|
222
|
+
}
|
|
223
|
+
export async function callQwenAPI(prompt, model = 'coder-model') {
|
|
224
|
+
const token = await loadToken();
|
|
225
|
+
if (!token) {
|
|
226
|
+
throw new Error('No hay token de Qwen. Ejecutá /login primero.');
|
|
227
|
+
}
|
|
228
|
+
// Qwen OAuth tokens work with chat.qwen.ai API
|
|
229
|
+
const baseUrl = 'https://chat.qwen.ai/api/v1';
|
|
230
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: {
|
|
233
|
+
'Authorization': `Bearer ${token.accessToken}`,
|
|
234
|
+
'Content-Type': 'application/json',
|
|
235
|
+
},
|
|
236
|
+
body: JSON.stringify({
|
|
237
|
+
model: model || 'coder-model',
|
|
238
|
+
messages: [
|
|
239
|
+
{ role: 'system', content: 'Sos el COORDINADOR de un equipo multi-agente de desarrollo. Tu trabajo es ENTENDER lo que el programador necesita haciendo PREGUNTAS si es necesario. Habla de forma NATURAL, como un compañero de equipo. Sé breve y directo.' },
|
|
240
|
+
{ role: 'user', content: prompt },
|
|
241
|
+
],
|
|
242
|
+
}),
|
|
243
|
+
});
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
const errorText = await response.text();
|
|
246
|
+
throw new Error(`Qwen API error: ${response.status} - ${errorText}`);
|
|
247
|
+
}
|
|
248
|
+
const data = await response.json();
|
|
249
|
+
return data.choices?.[0]?.message?.content || '';
|
|
250
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface SessionMessage {
|
|
2
|
+
role: 'user' | 'agent';
|
|
3
|
+
content: string;
|
|
4
|
+
ts: string;
|
|
5
|
+
}
|
|
6
|
+
export interface Session {
|
|
7
|
+
id: string;
|
|
8
|
+
dir: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
messages: SessionMessage[];
|
|
12
|
+
}
|
|
13
|
+
export declare function newSession(dir: string): Session;
|
|
14
|
+
export declare function saveSession(session: Session): Promise<void>;
|
|
15
|
+
export declare function loadSession(id: string): Promise<Session | null>;
|
|
16
|
+
export declare function listSessions(): Promise<Session[]>;
|
|
17
|
+
export declare function getLastSessionForDir(dir: string): Promise<Session | null>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { AGENT_HOME } from './config.js';
|
|
4
|
+
const SESSIONS_DIR = path.join(AGENT_HOME, 'sessions');
|
|
5
|
+
export function newSession(dir) {
|
|
6
|
+
const now = new Date().toISOString();
|
|
7
|
+
const id = now.replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
|
|
8
|
+
return { id, dir, createdAt: now, updatedAt: now, messages: [] };
|
|
9
|
+
}
|
|
10
|
+
export async function saveSession(session) {
|
|
11
|
+
await fs.mkdir(SESSIONS_DIR, { recursive: true });
|
|
12
|
+
session.updatedAt = new Date().toISOString();
|
|
13
|
+
await fs.writeFile(path.join(SESSIONS_DIR, `${session.id}.json`), JSON.stringify(session, null, 2) + '\n', 'utf-8');
|
|
14
|
+
}
|
|
15
|
+
export async function loadSession(id) {
|
|
16
|
+
try {
|
|
17
|
+
const content = await fs.readFile(path.join(SESSIONS_DIR, `${id}.json`), 'utf-8');
|
|
18
|
+
return JSON.parse(content);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function listSessions() {
|
|
25
|
+
try {
|
|
26
|
+
const files = await fs.readdir(SESSIONS_DIR);
|
|
27
|
+
const sessions = [];
|
|
28
|
+
for (const f of files) {
|
|
29
|
+
if (!f.endsWith('.json'))
|
|
30
|
+
continue;
|
|
31
|
+
try {
|
|
32
|
+
const content = await fs.readFile(path.join(SESSIONS_DIR, f), 'utf-8');
|
|
33
|
+
sessions.push(JSON.parse(content));
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
}
|
|
37
|
+
return sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function getLastSessionForDir(dir) {
|
|
44
|
+
const all = await listSessions();
|
|
45
|
+
return all.find((s) => s.dir === dir) ?? null;
|
|
46
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-rev",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reviewer agent — validates and reviews implementation against the plan",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/"
|
|
9
|
+
],
|
|
10
|
+
"bin": {
|
|
11
|
+
"agent-rev": "dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc && echo '#!/usr/bin/env node' | cat - dist/index.js > dist/index.tmp && mv dist/index.tmp dist/index.js && chmod +x dist/index.js",
|
|
15
|
+
"dev": "tsx src/index.ts",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ai",
|
|
20
|
+
"agent",
|
|
21
|
+
"orchestrator",
|
|
22
|
+
"multi-agent",
|
|
23
|
+
"cli",
|
|
24
|
+
"coding"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
29
|
+
"@google/generative-ai": "^0.24.0",
|
|
30
|
+
"chalk": "^5.4.1",
|
|
31
|
+
"commander": "^13.1.0",
|
|
32
|
+
"open": "^11.0.0",
|
|
33
|
+
"openai": "^4.91.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.13.0",
|
|
37
|
+
"@types/open": "^6.1.0",
|
|
38
|
+
"tsx": "^4.19.3",
|
|
39
|
+
"typescript": "^5.7.3"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|