agent-mp 0.4.6 → 0.4.8
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/core/engine.js +109 -38
- package/dist/utils/qwen-auth.d.ts +4 -8
- package/dist/utils/qwen-auth.js +50 -67
- package/package.json +3 -3
package/dist/core/engine.js
CHANGED
|
@@ -88,7 +88,7 @@ async function ask(prompt, rl, fi) {
|
|
|
88
88
|
const tempRl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
89
89
|
return new Promise((resolve) => tempRl.question(prompt, (a) => { tempRl.close(); resolve(a); }));
|
|
90
90
|
}
|
|
91
|
-
function runCli(cmd, prompt, timeoutMs = 600000, envOverride) {
|
|
91
|
+
function runCli(cmd, prompt, timeoutMs = 600000, envOverride, onData) {
|
|
92
92
|
// Si el comando es "qwen-direct", usar la API directa sin CLI
|
|
93
93
|
if (cmd.startsWith('qwen-direct')) {
|
|
94
94
|
const model = cmd.includes('-m') ? cmd.split('-m')[1]?.trim().split(/\s/)[0] : 'coder-model';
|
|
@@ -143,7 +143,7 @@ function runCli(cmd, prompt, timeoutMs = 600000, envOverride) {
|
|
|
143
143
|
});
|
|
144
144
|
let output = '';
|
|
145
145
|
let stderr = '';
|
|
146
|
-
child.stdout?.on('data', (d) => {
|
|
146
|
+
child.stdout?.on('data', (d) => { const s = d.toString(); output += s; onData?.(s); });
|
|
147
147
|
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
148
148
|
child.on('close', (code) => { resolve({ output: output + stderr, exitCode: code || 0 }); });
|
|
149
149
|
child.on('error', reject);
|
|
@@ -423,7 +423,10 @@ INSTRUCCIONES:
|
|
|
423
423
|
const tryWithAutoRepair = async (cliName, model, currentCmd) => {
|
|
424
424
|
const sp = this._startSpinner(`${cliName} ${model}`);
|
|
425
425
|
try {
|
|
426
|
-
const result = await runCli(currentCmd, rolePrompt)
|
|
426
|
+
const result = await runCli(currentCmd, rolePrompt, 600000, undefined, (chunk) => {
|
|
427
|
+
this._parseChunk(chunk).forEach(l => { if (l.trim())
|
|
428
|
+
sp.push(l); });
|
|
429
|
+
});
|
|
427
430
|
if (result.exitCode !== 0) {
|
|
428
431
|
sp.stop();
|
|
429
432
|
const detail = result.output.trim().slice(0, 500);
|
|
@@ -487,7 +490,10 @@ INSTRUCCIONES:
|
|
|
487
490
|
// Retry with new command
|
|
488
491
|
const sp2 = this._startSpinner(`${cliName} ${model} (retry)`);
|
|
489
492
|
try {
|
|
490
|
-
const result = await runCli(newCmd, rolePrompt)
|
|
493
|
+
const result = await runCli(newCmd, rolePrompt, 600000, undefined, (chunk) => {
|
|
494
|
+
this._parseChunk(chunk).forEach(l => { if (l.trim())
|
|
495
|
+
sp2.push(l); });
|
|
496
|
+
});
|
|
491
497
|
sp2.stop();
|
|
492
498
|
if (result.exitCode !== 0) {
|
|
493
499
|
const detail = result.output.trim().slice(0, 500);
|
|
@@ -515,17 +521,28 @@ INSTRUCCIONES:
|
|
|
515
521
|
return null;
|
|
516
522
|
}
|
|
517
523
|
const sp = this._startSpinner(`${cliName} ${model}`);
|
|
524
|
+
let lineBuf = '';
|
|
525
|
+
const onChunk = (delta) => {
|
|
526
|
+
lineBuf += delta;
|
|
527
|
+
const lines = lineBuf.split('\n');
|
|
528
|
+
lineBuf = lines.pop() || '';
|
|
529
|
+
for (const l of lines) {
|
|
530
|
+
if (l.trim())
|
|
531
|
+
sp.push(l.trim());
|
|
532
|
+
}
|
|
533
|
+
};
|
|
518
534
|
try {
|
|
519
535
|
log.info(`${cliName}: calling Qwen API with own credentials (${model})`);
|
|
520
|
-
const result = await callQwenAPIFromCreds(rolePrompt, model, credsPath,
|
|
536
|
+
const result = await callQwenAPIFromCreds(rolePrompt, model, credsPath, onChunk);
|
|
537
|
+
if (lineBuf.trim())
|
|
538
|
+
sp.push(lineBuf.trim());
|
|
521
539
|
sp.stop();
|
|
522
540
|
return result;
|
|
523
541
|
}
|
|
524
542
|
catch (err) {
|
|
525
543
|
sp.stop();
|
|
526
544
|
if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
|
|
527
|
-
|
|
528
|
-
console.log(chalk.yellow(` Ejecutá: ${cliName} --login\n`));
|
|
545
|
+
log.warn(`${cliName} session expired — using fallback`);
|
|
529
546
|
return null;
|
|
530
547
|
}
|
|
531
548
|
log.warn(`${cliName} direct API call failed: ${err.message}`);
|
|
@@ -566,15 +583,33 @@ INSTRUCCIONES:
|
|
|
566
583
|
}
|
|
567
584
|
}
|
|
568
585
|
}
|
|
569
|
-
// Try global fallback
|
|
586
|
+
// Try global fallback — use callQwenAPI directly for streaming (no subprocess)
|
|
570
587
|
if (this.config.fallback_global) {
|
|
571
588
|
const fb = this.config.fallback_global;
|
|
572
|
-
log.warn(`
|
|
573
|
-
const
|
|
574
|
-
|
|
589
|
+
log.warn(`Using global fallback: ${fb.cli} (${fb.model})`);
|
|
590
|
+
const sp = this._startSpinner(`${fb.cli} ${fb.model} (fallback)`);
|
|
591
|
+
let lineBuf = '';
|
|
592
|
+
const onChunk = (delta) => {
|
|
593
|
+
lineBuf += delta;
|
|
594
|
+
const lines = lineBuf.split('\n');
|
|
595
|
+
lineBuf = lines.pop() || '';
|
|
596
|
+
for (const l of lines) {
|
|
597
|
+
if (l.trim())
|
|
598
|
+
sp.push(l.trim());
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
try {
|
|
602
|
+
const globalResult = await callQwenAPI(rolePrompt, fb.model, onChunk);
|
|
603
|
+
if (lineBuf.trim())
|
|
604
|
+
sp.push(lineBuf.trim());
|
|
605
|
+
sp.stop();
|
|
575
606
|
trackTokens(globalResult, fb.cli, fb.model);
|
|
576
607
|
return globalResult;
|
|
577
608
|
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
sp.stop();
|
|
611
|
+
log.warn(`Global fallback failed: ${err.message}`);
|
|
612
|
+
}
|
|
578
613
|
}
|
|
579
614
|
// All options exhausted
|
|
580
615
|
throw new Error(`${roleName} failed: primary + individual fallback + global fallback all failed`);
|
|
@@ -585,10 +620,10 @@ INSTRUCCIONES:
|
|
|
585
620
|
const preambles = {
|
|
586
621
|
orchestrator: `ROL: ORCHESTRATOR — solo planifica, nunca implementa.
|
|
587
622
|
REGLA: Tu unica salida debe ser el JSON del plan. Sin explicaciones, sin texto extra.`,
|
|
588
|
-
implementor: `ROL: IMPLEMENTOR —
|
|
589
|
-
REGLA:
|
|
590
|
-
REGLA: NO tomes decisiones de arquitectura.
|
|
591
|
-
REGLA:
|
|
623
|
+
implementor: `ROL: IMPLEMENTOR — genera el contenido completo de los archivos indicados.
|
|
624
|
+
REGLA: Para cada archivo, usa el bloque === FILE: ruta === ... === END FILE === exactamente.
|
|
625
|
+
REGLA: NO tomes decisiones de arquitectura. Sigue los steps del plan.
|
|
626
|
+
REGLA: Genera el contenido COMPLETO de cada archivo, no fragmentos ni pseudocodigo.`,
|
|
592
627
|
reviewer: `ROL: REVIEWER — solo valida, nunca modifica codigo.
|
|
593
628
|
REGLA: Usa tus herramientas (read_file, grep_search, etc.) para leer los archivos y verificar criterios.
|
|
594
629
|
REGLA: NO modifiques archivos. NO implementes correcciones.
|
|
@@ -739,41 +774,77 @@ INSTRUCCIONES:
|
|
|
739
774
|
if (await fileExists(structurePath)) {
|
|
740
775
|
structureRules = await readFile(structurePath);
|
|
741
776
|
}
|
|
742
|
-
//
|
|
777
|
+
// Read architecture context for reference
|
|
778
|
+
const context = await this.buildOrchestratorContext();
|
|
779
|
+
let archContext = '';
|
|
780
|
+
const archPath = path.join(this.projectDir, '.agent', 'context', 'architecture.md');
|
|
781
|
+
if (await fileExists(archPath)) {
|
|
782
|
+
archContext = (await readFile(archPath)).slice(0, 3000);
|
|
783
|
+
}
|
|
784
|
+
const allTargetFiles = [...new Set(plan.steps.flatMap((s) => s.files || []))];
|
|
743
785
|
const stepsText = plan.steps.map((s) => `Step ${s.num}: ${s.description}${s.files?.length ? `\n Archivos: ${s.files.join(', ')}` : ''}`).join('\n');
|
|
786
|
+
// The engine writes files itself — ask the LLM to generate content in parseable blocks.
|
|
787
|
+
// This avoids depending on CLI tool-use capabilities which are unavailable in API mode.
|
|
744
788
|
const prompt = `TAREA: ${plan.description}
|
|
745
789
|
DIRECTORIO_TRABAJO: ${this.projectDir}
|
|
746
790
|
|
|
747
|
-
STEPS A
|
|
791
|
+
STEPS A IMPLEMENTAR EN ORDEN:
|
|
748
792
|
${stepsText}
|
|
749
793
|
|
|
794
|
+
${allTargetFiles.length ? `ARCHIVOS A CREAR/MODIFICAR:\n${allTargetFiles.map(f => `- ${f}`).join('\n')}` : ''}
|
|
795
|
+
|
|
796
|
+
${context ? `CONTEXTO DEL PROYECTO:\n${context.slice(0, 2000)}\n` : ''}
|
|
797
|
+
${archContext ? `ARQUITECTURA EXISTENTE:\n${archContext}\n` : ''}
|
|
750
798
|
${structureRules ? `REGLAS DE ESTRUCTURA:\n${structureRules}\n` : ''}
|
|
751
|
-
INSTRUCCIONES:
|
|
752
|
-
1. Ejecuta cada step EN ORDEN usando tus herramientas (read_file, write_file, run_shell_command, etc.)
|
|
753
|
-
2. Para cada step: lee lo necesario, luego crea/modifica los archivos indicados.
|
|
754
|
-
3. NO tomes decisiones de arquitectura fuera del plan.
|
|
755
|
-
4. Al terminar todos los steps, responde SOLO con este JSON:
|
|
756
799
|
|
|
757
|
-
|
|
800
|
+
INSTRUCCIONES CRITICAS:
|
|
801
|
+
1. Para CADA archivo que debes crear/modificar, usa este formato EXACTO:
|
|
802
|
+
|
|
803
|
+
=== FILE: ruta/relativa/del/archivo ===
|
|
804
|
+
contenido completo del archivo aqui
|
|
805
|
+
=== END FILE ===
|
|
806
|
+
|
|
807
|
+
2. Las rutas son RELATIVAS al DIRECTORIO_TRABAJO.
|
|
808
|
+
3. Genera TODOS los archivos necesarios — uno por bloque FILE.
|
|
809
|
+
4. Incluye el contenido completo de cada archivo (no fragmentos).
|
|
810
|
+
5. Si un step modifica un archivo existente, incluye el archivo completo con los cambios.
|
|
811
|
+
6. NO incluyas explicaciones fuera de los bloques FILE.`;
|
|
758
812
|
const res = await this.runWithFallback('implementor', prompt, 'Implementacion');
|
|
759
|
-
// Extract text from streaming CLI output (qwen/claude return event arrays)
|
|
760
813
|
const text = extractCliText(res);
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
814
|
+
// Parse === FILE: path === ... === END FILE === blocks written by the LLM
|
|
815
|
+
const fileBlocks = [];
|
|
816
|
+
const fileBlockRegex = /^=== FILE: (.+?) ===\n([\s\S]*?)\n=== END FILE ===/gm;
|
|
817
|
+
let match;
|
|
818
|
+
while ((match = fileBlockRegex.exec(text)) !== null) {
|
|
819
|
+
fileBlocks.push({ relPath: match[1].trim(), content: match[2] });
|
|
765
820
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
821
|
+
const now = new Date().toISOString();
|
|
822
|
+
if (fileBlocks.length > 0) {
|
|
823
|
+
const sp = this._startSpinner(`Escribiendo ${fileBlocks.length} archivo(s)`);
|
|
824
|
+
for (const { relPath, content } of fileBlocks) {
|
|
825
|
+
// Strip markdown code fences the LLM may have wrapped around content
|
|
826
|
+
const clean = content.replace(/^```[\w]*\n/, '').replace(/\n```$/, '');
|
|
827
|
+
sp.push(relPath);
|
|
828
|
+
const absPath = path.join(this.projectDir, relPath);
|
|
829
|
+
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
830
|
+
await writeFile(absPath, clean);
|
|
831
|
+
}
|
|
832
|
+
sp.stop();
|
|
833
|
+
log.ok(`${fileBlocks.length} archivo(s) implementado(s)`);
|
|
776
834
|
}
|
|
835
|
+
else {
|
|
836
|
+
log.warn('Implementor did not return FILE blocks — no files written');
|
|
837
|
+
}
|
|
838
|
+
const progress = {
|
|
839
|
+
task_id: taskId,
|
|
840
|
+
created_at: now,
|
|
841
|
+
status: fileBlocks.length > 0 ? 'completed' : 'failed',
|
|
842
|
+
steps: plan.steps.map((s) => ({
|
|
843
|
+
...s,
|
|
844
|
+
status: fileBlocks.length > 0 ? 'completed' : 'failed',
|
|
845
|
+
completed_at: now,
|
|
846
|
+
})),
|
|
847
|
+
};
|
|
777
848
|
await writeJson(path.join(taskDir, 'progress.json'), progress);
|
|
778
849
|
log.ok('progress.json updated');
|
|
779
850
|
return progress;
|
|
@@ -11,16 +11,12 @@ export declare function qwenAuthStatus(): Promise<{
|
|
|
11
11
|
export declare function fetchQwenModels(): Promise<string[]>;
|
|
12
12
|
export declare function getQwenAccessToken(): Promise<string | null>;
|
|
13
13
|
/**
|
|
14
|
-
* Call Qwen
|
|
15
|
-
*
|
|
16
|
-
* Falls back to direct HTTP call if the qwen CLI is not available.
|
|
14
|
+
* Call Qwen REST API directly using OAuth credentials stored locally.
|
|
15
|
+
* No dependency on any qwen CLI binary.
|
|
17
16
|
*/
|
|
18
17
|
export declare function callQwenAPI(prompt: string, model?: string, onData?: (chunk: string) => void): Promise<string>;
|
|
19
18
|
/**
|
|
20
|
-
* Call Qwen API using credentials
|
|
21
|
-
*
|
|
22
|
-
* shared ~/.qwen/oauth_creds.json — we spawn it with piped stdin so it runs
|
|
23
|
-
* in non-interactive mode without TTY issues.
|
|
24
|
-
* Falls back to direct HTTP if the role binary is not found.
|
|
19
|
+
* Call Qwen API using credentials stored at a specific path (for role binaries).
|
|
20
|
+
* Calls the Qwen REST API directly — no dependency on any qwen CLI binary.
|
|
25
21
|
*/
|
|
26
22
|
export declare function callQwenAPIFromCreds(prompt: string, model: string, credsPath: string, onData?: (chunk: string) => void): Promise<string>;
|
package/dist/utils/qwen-auth.js
CHANGED
|
@@ -1,25 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as crypto from 'crypto';
|
|
4
|
-
import { spawn } from 'child_process';
|
|
5
|
-
/** Async alternative to spawnSync — keeps the event loop free so UI can update. */
|
|
6
|
-
function spawnAsync(bin, args, input, timeout, onData) {
|
|
7
|
-
return new Promise((resolve, reject) => {
|
|
8
|
-
const child = spawn(bin, args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
9
|
-
let stdout = '';
|
|
10
|
-
child.stdout?.on('data', (d) => {
|
|
11
|
-
const s = d.toString();
|
|
12
|
-
stdout += s;
|
|
13
|
-
onData?.(s);
|
|
14
|
-
});
|
|
15
|
-
child.stderr?.on('data', (d) => { stdout += d.toString(); });
|
|
16
|
-
const timer = setTimeout(() => { child.kill(); reject(new Error('qwen timeout')); }, timeout);
|
|
17
|
-
child.on('close', (code) => { clearTimeout(timer); resolve({ stdout, status: code ?? 0 }); });
|
|
18
|
-
child.on('error', (err) => { clearTimeout(timer); reject(err); });
|
|
19
|
-
child.stdin?.write(input, 'utf-8');
|
|
20
|
-
child.stdin?.end();
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
4
|
import open from 'open';
|
|
24
5
|
import { AGENT_HOME } from './config.js';
|
|
25
6
|
const QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai';
|
|
@@ -267,8 +248,9 @@ export async function getQwenAccessToken() {
|
|
|
267
248
|
const token = await loadToken();
|
|
268
249
|
return token?.accessToken || null;
|
|
269
250
|
}
|
|
270
|
-
async function callQwenAPIWithToken(token, prompt, model) {
|
|
251
|
+
async function callQwenAPIWithToken(token, prompt, model, onData) {
|
|
271
252
|
const baseUrl = 'https://chat.qwen.ai/api/v1';
|
|
253
|
+
const useStream = !!onData;
|
|
272
254
|
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
273
255
|
method: 'POST',
|
|
274
256
|
headers: {
|
|
@@ -277,46 +259,65 @@ async function callQwenAPIWithToken(token, prompt, model) {
|
|
|
277
259
|
},
|
|
278
260
|
body: JSON.stringify({
|
|
279
261
|
model: model || 'coder-model',
|
|
280
|
-
messages: [
|
|
281
|
-
|
|
282
|
-
],
|
|
262
|
+
messages: [{ role: 'user', content: prompt }],
|
|
263
|
+
stream: useStream,
|
|
283
264
|
}),
|
|
284
265
|
});
|
|
285
266
|
if (!response.ok) {
|
|
286
267
|
const errorText = await response.text();
|
|
287
|
-
if (response.status === 401)
|
|
268
|
+
if (response.status === 401)
|
|
288
269
|
throw new Error(`QWEN_AUTH_EXPIRED: ${errorText}`);
|
|
289
|
-
}
|
|
290
270
|
throw new Error(`Qwen API error: ${response.status} - ${errorText}`);
|
|
291
271
|
}
|
|
292
|
-
|
|
293
|
-
|
|
272
|
+
if (!useStream) {
|
|
273
|
+
const data = await response.json();
|
|
274
|
+
return data.choices?.[0]?.message?.content || '';
|
|
275
|
+
}
|
|
276
|
+
// Streaming — parse SSE chunks
|
|
277
|
+
const reader = response.body?.getReader();
|
|
278
|
+
if (!reader)
|
|
279
|
+
throw new Error('No response body for streaming');
|
|
280
|
+
const decoder = new TextDecoder();
|
|
281
|
+
let fullText = '';
|
|
282
|
+
let buffer = '';
|
|
283
|
+
while (true) {
|
|
284
|
+
const { done, value } = await reader.read();
|
|
285
|
+
if (done)
|
|
286
|
+
break;
|
|
287
|
+
buffer += decoder.decode(value, { stream: true });
|
|
288
|
+
const lines = buffer.split('\n');
|
|
289
|
+
buffer = lines.pop() || '';
|
|
290
|
+
for (const line of lines) {
|
|
291
|
+
const trimmed = line.trim();
|
|
292
|
+
if (!trimmed.startsWith('data: '))
|
|
293
|
+
continue;
|
|
294
|
+
const data = trimmed.slice(6);
|
|
295
|
+
if (data === '[DONE]')
|
|
296
|
+
continue;
|
|
297
|
+
try {
|
|
298
|
+
const parsed = JSON.parse(data);
|
|
299
|
+
const delta = parsed.choices?.[0]?.delta?.content || '';
|
|
300
|
+
if (delta) {
|
|
301
|
+
fullText += delta;
|
|
302
|
+
onData(delta);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch { /* skip malformed chunks */ }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return fullText;
|
|
294
309
|
}
|
|
295
310
|
/**
|
|
296
|
-
* Call Qwen
|
|
297
|
-
*
|
|
298
|
-
* Falls back to direct HTTP call if the qwen CLI is not available.
|
|
311
|
+
* Call Qwen REST API directly using OAuth credentials stored locally.
|
|
312
|
+
* No dependency on any qwen CLI binary.
|
|
299
313
|
*/
|
|
300
314
|
export async function callQwenAPI(prompt, model = 'coder-model', onData) {
|
|
301
|
-
// Try using the qwen CLI subprocess first — it handles auth/refresh/format automatically
|
|
302
|
-
const qwenBin = process.env.QWEN_BIN || 'qwen';
|
|
303
|
-
try {
|
|
304
|
-
const result = await spawnAsync(qwenBin, [], prompt, 300000, onData);
|
|
305
|
-
if (result.status === 0 && result.stdout.trim()) {
|
|
306
|
-
return result.stdout.trim();
|
|
307
|
-
}
|
|
308
|
-
// qwen not available or failed — fall through to direct API
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
// qwen not installed — fall through
|
|
312
|
-
}
|
|
313
|
-
// Fallback: direct API call (requires valid token in AGENT_HOME)
|
|
314
315
|
let token = await loadToken();
|
|
315
316
|
if (!token) {
|
|
316
317
|
throw new Error('QWEN_AUTH_EXPIRED: No hay token de Qwen. Ejecutá --login primero.');
|
|
317
318
|
}
|
|
318
319
|
try {
|
|
319
|
-
return await callQwenAPIWithToken(token, prompt, model);
|
|
320
|
+
return await callQwenAPIWithToken(token, prompt, model, onData);
|
|
320
321
|
}
|
|
321
322
|
catch (err) {
|
|
322
323
|
if (!err.message?.startsWith('QWEN_AUTH_EXPIRED'))
|
|
@@ -325,34 +326,18 @@ export async function callQwenAPI(prompt, model = 'coder-model', onData) {
|
|
|
325
326
|
const refreshed = await doRefreshToken(token.refreshToken);
|
|
326
327
|
if (refreshed) {
|
|
327
328
|
await saveToken(refreshed);
|
|
328
|
-
return callQwenAPIWithToken(refreshed, prompt, model);
|
|
329
|
+
return callQwenAPIWithToken(refreshed, prompt, model, onData);
|
|
329
330
|
}
|
|
330
331
|
}
|
|
331
332
|
throw new Error('QWEN_AUTH_EXPIRED: Sesión expirada. Ejecutá: agent-mp --login');
|
|
332
333
|
}
|
|
333
334
|
}
|
|
334
335
|
/**
|
|
335
|
-
* Call Qwen API using credentials
|
|
336
|
-
*
|
|
337
|
-
* shared ~/.qwen/oauth_creds.json — we spawn it with piped stdin so it runs
|
|
338
|
-
* in non-interactive mode without TTY issues.
|
|
339
|
-
* Falls back to direct HTTP if the role binary is not found.
|
|
336
|
+
* Call Qwen API using credentials stored at a specific path (for role binaries).
|
|
337
|
+
* Calls the Qwen REST API directly — no dependency on any qwen CLI binary.
|
|
340
338
|
*/
|
|
341
339
|
export async function callQwenAPIFromCreds(prompt, model, credsPath, onData) {
|
|
342
|
-
// Derive the role binary name from the creds path (e.g. ~/.agent-explorer/ → agent-explorer)
|
|
343
340
|
const cliName = path.basename(path.dirname(credsPath)).replace(/^\./, '');
|
|
344
|
-
// Try spawning the qwen CLI with piped stdin (async — keeps event loop free)
|
|
345
|
-
const qwenBin = process.env.QWEN_BIN || 'qwen';
|
|
346
|
-
try {
|
|
347
|
-
const result = await spawnAsync(qwenBin, [], prompt, 300000, onData);
|
|
348
|
-
if (result.status === 0 && result.stdout.trim()) {
|
|
349
|
-
return result.stdout.trim();
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
catch {
|
|
353
|
-
// qwen not available
|
|
354
|
-
}
|
|
355
|
-
// Fallback: direct HTTP with stored creds
|
|
356
341
|
let raw;
|
|
357
342
|
try {
|
|
358
343
|
raw = JSON.parse(await fs.readFile(credsPath, 'utf-8'));
|
|
@@ -370,7 +355,6 @@ export async function callQwenAPIFromCreds(prompt, model, credsPath, onData) {
|
|
|
370
355
|
if (!token.accessToken) {
|
|
371
356
|
throw new Error(`Invalid credentials at ${credsPath}. Run: ${cliName} --login`);
|
|
372
357
|
}
|
|
373
|
-
// Refresh proactivo: si vence en menos de 2 minutos (o ya venció)
|
|
374
358
|
const TWO_MIN = 2 * 60 * 1000;
|
|
375
359
|
if (token.expiresAt - Date.now() < TWO_MIN && token.refreshToken) {
|
|
376
360
|
const refreshed = await doRefreshToken(token.refreshToken);
|
|
@@ -380,17 +364,16 @@ export async function callQwenAPIFromCreds(prompt, model, credsPath, onData) {
|
|
|
380
364
|
}
|
|
381
365
|
}
|
|
382
366
|
try {
|
|
383
|
-
return await callQwenAPIWithToken(token, prompt, model);
|
|
367
|
+
return await callQwenAPIWithToken(token, prompt, model, onData);
|
|
384
368
|
}
|
|
385
369
|
catch (err) {
|
|
386
370
|
if (!err.message?.startsWith('QWEN_AUTH_EXPIRED'))
|
|
387
371
|
throw err;
|
|
388
|
-
// 401 server-side — intentar refresh aunque expiresAt no haya vencido
|
|
389
372
|
if (token.refreshToken) {
|
|
390
373
|
const refreshed = await doRefreshToken(token.refreshToken);
|
|
391
374
|
if (refreshed) {
|
|
392
375
|
await fs.writeFile(credsPath, JSON.stringify(refreshed, null, 2), 'utf-8');
|
|
393
|
-
return callQwenAPIWithToken(refreshed, prompt, model);
|
|
376
|
+
return callQwenAPIWithToken(refreshed, prompt, model, onData);
|
|
394
377
|
}
|
|
395
378
|
}
|
|
396
379
|
throw new Error(`QWEN_AUTH_EXPIRED: Sesión expirada. Ejecutá: ${cliName} --login`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-mp",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Deterministic multi-agent CLI orchestrator
|
|
3
|
+
"version": "0.4.8",
|
|
4
|
+
"description": "Deterministic multi-agent CLI orchestrator — plan, code, review",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"files": [
|
|
@@ -41,4 +41,4 @@
|
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=18.0.0"
|
|
43
43
|
}
|
|
44
|
-
}
|
|
44
|
+
}
|