agent-mp 0.4.0 → 0.4.2
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.d.ts +11 -1
- package/dist/core/engine.js +124 -25
- package/dist/ui/input.d.ts +15 -9
- package/dist/ui/input.js +105 -71
- package/dist/utils/qwen-auth.d.ts +2 -2
- package/dist/utils/qwen-auth.js +26 -18
- package/package.json +1 -1
package/dist/core/engine.d.ts
CHANGED
|
@@ -16,8 +16,10 @@ export declare class AgentEngine {
|
|
|
16
16
|
private totalTokens;
|
|
17
17
|
private phaseTokens;
|
|
18
18
|
constructor(config: AgentConfig, projectDir: string, coordinatorCmd?: string, rl?: readline.Interface, fi?: FixedInput, slashHandler?: SlashHandler);
|
|
19
|
-
/** Start
|
|
19
|
+
/** Start the activity box for a subagent call. Returns { stop, push }. */
|
|
20
20
|
private _startSpinner;
|
|
21
|
+
/** Extract readable text lines from a qwen/CLI streaming chunk. */
|
|
22
|
+
private _parseChunk;
|
|
21
23
|
/**
|
|
22
24
|
* FASE 0 — Clarificacion con el programador.
|
|
23
25
|
* El coordinador (CLI activo, ej: Qwen) conversa con el usuario
|
|
@@ -40,6 +42,14 @@ export declare class AgentEngine {
|
|
|
40
42
|
runReviewer(taskId: string, plan: TaskPlan, progress: TaskProgress): Promise<{
|
|
41
43
|
verdict: string;
|
|
42
44
|
}>;
|
|
45
|
+
/**
|
|
46
|
+
* Scan contextDir for stray files (outside the allowed structure) and clean them up:
|
|
47
|
+
* - Old timestamped explorer-*.md reports → delete (content already in architecture.md)
|
|
48
|
+
* - Other stray .md files at root level → append content to architecture.md, then delete
|
|
49
|
+
* Allowed root-level files: architecture.md, explorer-last.md
|
|
50
|
+
* Allowed subdirectory files: <service>/architecture.md (not touched here)
|
|
51
|
+
*/
|
|
52
|
+
private _cleanContextDir;
|
|
43
53
|
runExplorer(task?: string): Promise<string>;
|
|
44
54
|
/** Called when the current binary IS the configured explorer CLI (prevents recursion).
|
|
45
55
|
* Builds the full exploration prompt and calls Qwen API using own credentials. */
|
package/dist/core/engine.js
CHANGED
|
@@ -219,19 +219,54 @@ export class AgentEngine {
|
|
|
219
219
|
this.fi = fi;
|
|
220
220
|
this.slashHandler = slashHandler;
|
|
221
221
|
}
|
|
222
|
-
/** Start
|
|
222
|
+
/** Start the activity box for a subagent call. Returns { stop, push }. */
|
|
223
223
|
_startSpinner(label) {
|
|
224
|
+
const noop = { stop() { }, push(_) { } };
|
|
224
225
|
if (!this.fi)
|
|
225
|
-
return
|
|
226
|
+
return noop;
|
|
226
227
|
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
227
228
|
let i = 0;
|
|
228
229
|
const t0 = Date.now();
|
|
229
230
|
const fi = this.fi;
|
|
231
|
+
fi.startActivity(`${frames[0]} ${label} 0s`);
|
|
230
232
|
const iv = setInterval(() => {
|
|
231
233
|
const s = Math.floor((Date.now() - t0) / 1000);
|
|
232
|
-
fi.
|
|
234
|
+
fi.updateActivityHeader(`${frames[i++ % frames.length]} ${label} ${s}s`);
|
|
233
235
|
}, 100);
|
|
234
|
-
return
|
|
236
|
+
return {
|
|
237
|
+
stop() { clearInterval(iv); fi.stopActivity(); },
|
|
238
|
+
push(line) { fi.pushActivity(line); },
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/** Extract readable text lines from a qwen/CLI streaming chunk. */
|
|
242
|
+
_parseChunk(chunk) {
|
|
243
|
+
const out = [];
|
|
244
|
+
for (const raw of chunk.split('\n')) {
|
|
245
|
+
const line = raw.trim();
|
|
246
|
+
if (!line)
|
|
247
|
+
continue;
|
|
248
|
+
// Try to extract text from JSON streaming events
|
|
249
|
+
if (line.startsWith('{')) {
|
|
250
|
+
try {
|
|
251
|
+
const obj = JSON.parse(line);
|
|
252
|
+
const text = obj.result ||
|
|
253
|
+
obj.data?.content ||
|
|
254
|
+
obj.message?.content?.[0]?.text ||
|
|
255
|
+
obj.choices?.[0]?.delta?.content ||
|
|
256
|
+
obj.choices?.[0]?.message?.content ||
|
|
257
|
+
'';
|
|
258
|
+
if (text.trim())
|
|
259
|
+
out.push(text.trim());
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
catch { /* not JSON — fall through */ }
|
|
263
|
+
}
|
|
264
|
+
// Skip lines that look like SSE metadata
|
|
265
|
+
if (line.startsWith('data:') || line.startsWith('event:') || line === '[DONE]')
|
|
266
|
+
continue;
|
|
267
|
+
out.push(line);
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
235
270
|
}
|
|
236
271
|
/**
|
|
237
272
|
* FASE 0 — Clarificacion con el programador.
|
|
@@ -268,14 +303,14 @@ INSTRUCCIONES:
|
|
|
268
303
|
// Use Qwen API directly — avoids the qwen CLI's own OAuth flow
|
|
269
304
|
// which causes mid-session auth popups and breaks display.
|
|
270
305
|
const model = this.coordinatorCmd.match(/(?:-m|--model)\s+(\S+)/)?.[1] || 'coder-model';
|
|
271
|
-
const
|
|
306
|
+
const sp = this._startSpinner(`coordinador ${model}`);
|
|
272
307
|
try {
|
|
273
|
-
const result = await callQwenAPI(prompt, model);
|
|
274
|
-
|
|
308
|
+
const result = await callQwenAPI(prompt, model, (c) => this._parseChunk(c).forEach(l => sp.push(l)));
|
|
309
|
+
sp.stop();
|
|
275
310
|
return result;
|
|
276
311
|
}
|
|
277
312
|
catch (err) {
|
|
278
|
-
|
|
313
|
+
sp.stop();
|
|
279
314
|
if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
|
|
280
315
|
console.log(chalk.red('\n ✗ Sesión Qwen expirada.'));
|
|
281
316
|
console.log(chalk.yellow(' Ejecutá: /login para re-autenticarte.\n'));
|
|
@@ -287,9 +322,9 @@ INSTRUCCIONES:
|
|
|
287
322
|
}
|
|
288
323
|
}
|
|
289
324
|
else {
|
|
290
|
-
const
|
|
325
|
+
const sp = this._startSpinner(`coordinador`);
|
|
291
326
|
res = await runCli(this.coordinatorCmd, prompt, 600000, envOverride);
|
|
292
|
-
|
|
327
|
+
sp.stop();
|
|
293
328
|
}
|
|
294
329
|
// Extract readable text — search for JSON array even if there's prefix text
|
|
295
330
|
let responseText = res.output.trim();
|
|
@@ -386,19 +421,19 @@ INSTRUCCIONES:
|
|
|
386
421
|
const rolePrompt = this.buildRolePrompt(roleName, prompt);
|
|
387
422
|
/** Try a cmd, and if it fails, auto-detect flags from --help and retry */
|
|
388
423
|
const tryWithAutoRepair = async (cliName, model, currentCmd) => {
|
|
389
|
-
const
|
|
424
|
+
const sp = this._startSpinner(`${cliName} ${model}`);
|
|
390
425
|
try {
|
|
391
426
|
const result = await runCli(currentCmd, rolePrompt);
|
|
392
427
|
if (result.exitCode !== 0) {
|
|
393
|
-
|
|
428
|
+
sp.stop();
|
|
394
429
|
const detail = result.output.trim().slice(0, 500);
|
|
395
430
|
throw new Error(`${cliName} exited with code ${result.exitCode}${detail ? `\n${detail}` : ''}`);
|
|
396
431
|
}
|
|
397
|
-
|
|
432
|
+
sp.stop();
|
|
398
433
|
return result.output;
|
|
399
434
|
}
|
|
400
435
|
catch (err) {
|
|
401
|
-
|
|
436
|
+
sp.stop();
|
|
402
437
|
log.warn(`${cliName} failed, detecting flags from --help: ${err.message}`);
|
|
403
438
|
const detected = detectCliFlags(cliName);
|
|
404
439
|
if (Object.keys(detected).length === 0) {
|
|
@@ -450,10 +485,10 @@ INSTRUCCIONES:
|
|
|
450
485
|
// Config file might not exist yet
|
|
451
486
|
}
|
|
452
487
|
// Retry with new command
|
|
453
|
-
const
|
|
488
|
+
const sp2 = this._startSpinner(`${cliName} ${model} (retry)`);
|
|
454
489
|
try {
|
|
455
490
|
const result = await runCli(newCmd, rolePrompt);
|
|
456
|
-
|
|
491
|
+
sp2.stop();
|
|
457
492
|
if (result.exitCode !== 0) {
|
|
458
493
|
const detail = result.output.trim().slice(0, 500);
|
|
459
494
|
throw new Error(`${cliName} (repaired) exited with code ${result.exitCode}${detail ? `\n${detail}` : ''}`);
|
|
@@ -462,7 +497,7 @@ INSTRUCCIONES:
|
|
|
462
497
|
return result.output;
|
|
463
498
|
}
|
|
464
499
|
catch (retryErr) {
|
|
465
|
-
|
|
500
|
+
sp2.stop();
|
|
466
501
|
log.warn(`Repaired ${cliName} also failed: ${retryErr.message}`);
|
|
467
502
|
return null;
|
|
468
503
|
}
|
|
@@ -479,15 +514,15 @@ INSTRUCCIONES:
|
|
|
479
514
|
log.warn(`${cliName} has no credentials — run: ${cliName} --login`);
|
|
480
515
|
return null;
|
|
481
516
|
}
|
|
482
|
-
const
|
|
517
|
+
const sp = this._startSpinner(`${cliName} ${model}`);
|
|
483
518
|
try {
|
|
484
519
|
log.info(`${cliName}: calling Qwen API with own credentials (${model})`);
|
|
485
|
-
const result = await callQwenAPIFromCreds(rolePrompt, model, credsPath);
|
|
486
|
-
|
|
520
|
+
const result = await callQwenAPIFromCreds(rolePrompt, model, credsPath, (c) => this._parseChunk(c).forEach(l => sp.push(l)));
|
|
521
|
+
sp.stop();
|
|
487
522
|
return result;
|
|
488
523
|
}
|
|
489
524
|
catch (err) {
|
|
490
|
-
|
|
525
|
+
sp.stop();
|
|
491
526
|
if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
|
|
492
527
|
console.log(chalk.red(`\n ✗ Sesión expirada para ${cliName}.`));
|
|
493
528
|
console.log(chalk.yellow(` Ejecutá: ${cliName} --login\n`));
|
|
@@ -799,6 +834,66 @@ INSTRUCCIONES:
|
|
|
799
834
|
log.verdict(verdict);
|
|
800
835
|
return { verdict };
|
|
801
836
|
}
|
|
837
|
+
/**
|
|
838
|
+
* Scan contextDir for stray files (outside the allowed structure) and clean them up:
|
|
839
|
+
* - Old timestamped explorer-*.md reports → delete (content already in architecture.md)
|
|
840
|
+
* - Other stray .md files at root level → append content to architecture.md, then delete
|
|
841
|
+
* Allowed root-level files: architecture.md, explorer-last.md
|
|
842
|
+
* Allowed subdirectory files: <service>/architecture.md (not touched here)
|
|
843
|
+
*/
|
|
844
|
+
async _cleanContextDir(contextDir) {
|
|
845
|
+
let entries;
|
|
846
|
+
try {
|
|
847
|
+
entries = await fs.readdir(contextDir, { withFileTypes: true });
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
return;
|
|
851
|
+
} // dir doesn't exist yet
|
|
852
|
+
const archPath = path.join(contextDir, 'architecture.md');
|
|
853
|
+
const stray = [];
|
|
854
|
+
for (const e of entries) {
|
|
855
|
+
if (!e.isFile())
|
|
856
|
+
continue;
|
|
857
|
+
if (!e.name.endsWith('.md'))
|
|
858
|
+
continue;
|
|
859
|
+
if (e.name === 'architecture.md' || e.name === 'explorer-last.md')
|
|
860
|
+
continue;
|
|
861
|
+
stray.push(e.name);
|
|
862
|
+
}
|
|
863
|
+
if (stray.length === 0)
|
|
864
|
+
return;
|
|
865
|
+
log.info(`Limpiando ${stray.length} archivo(s) fuera de estructura en .agent/context/`);
|
|
866
|
+
for (const name of stray) {
|
|
867
|
+
const filePath = path.join(contextDir, name);
|
|
868
|
+
// Old timestamped explorer reports — just delete (content is redundant)
|
|
869
|
+
if (/^explorer-\d{4}-\d{2}-/.test(name)) {
|
|
870
|
+
await fs.unlink(filePath);
|
|
871
|
+
log.ok(` Eliminado: ${name}`);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
// Other stray files — migrate content to architecture.md, then delete
|
|
875
|
+
try {
|
|
876
|
+
const content = (await readFile(filePath)).trim();
|
|
877
|
+
if (content) {
|
|
878
|
+
let arch = '';
|
|
879
|
+
try {
|
|
880
|
+
arch = await readFile(archPath);
|
|
881
|
+
}
|
|
882
|
+
catch { /* no arch yet */ }
|
|
883
|
+
const label = name.replace(/\.md$/, '');
|
|
884
|
+
await writeFile(archPath, `${arch}\n\n---\n## ${label}\n\n${content}\n`);
|
|
885
|
+
log.ok(` Migrado: ${name} → architecture.md`);
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
log.ok(` Eliminado (vacío): ${name}`);
|
|
889
|
+
}
|
|
890
|
+
await fs.unlink(filePath);
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
log.warn(` No se pudo limpiar ${name}: ${err.message}`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
802
897
|
async runExplorer(task) {
|
|
803
898
|
if (!this.config.roles.explorer) {
|
|
804
899
|
if (!this.config.fallback_global) {
|
|
@@ -813,6 +908,8 @@ INSTRUCCIONES:
|
|
|
813
908
|
const contextDir = path.join(agentDir, 'context');
|
|
814
909
|
await fs.mkdir(contextDir, { recursive: true });
|
|
815
910
|
await fs.mkdir(path.join(agentDir, 'rules'), { recursive: true });
|
|
911
|
+
// Clean up stray files before running
|
|
912
|
+
await this._cleanContextDir(contextDir);
|
|
816
913
|
// Read existing architecture doc if any
|
|
817
914
|
const archPath = path.join(contextDir, 'architecture.md');
|
|
818
915
|
let existingArch = '';
|
|
@@ -863,6 +960,8 @@ REGLAS:
|
|
|
863
960
|
const agentDir = path.join(this.projectDir, '.agent');
|
|
864
961
|
const contextDir = path.join(agentDir, 'context');
|
|
865
962
|
await fs.mkdir(contextDir, { recursive: true });
|
|
963
|
+
// Clean up stray files before running
|
|
964
|
+
await this._cleanContextDir(contextDir);
|
|
866
965
|
const archPath = path.join(contextDir, 'architecture.md');
|
|
867
966
|
let existingArch = '';
|
|
868
967
|
try {
|
|
@@ -889,13 +988,13 @@ REGLAS:
|
|
|
889
988
|
- Escribe UNICAMENTE los archivos indicados: ${archPath} y ${contextDir}/<servicio>/architecture.md
|
|
890
989
|
- NO crees archivos adicionales ni con otros nombres en ningun directorio`);
|
|
891
990
|
let result;
|
|
892
|
-
const
|
|
991
|
+
const sp = this._startSpinner(`agent-explorer ${role.model}`);
|
|
893
992
|
try {
|
|
894
|
-
result = await callQwenAPI(prompt, role.model);
|
|
895
|
-
|
|
993
|
+
result = await callQwenAPI(prompt, role.model, (c) => this._parseChunk(c).forEach(l => sp.push(l)));
|
|
994
|
+
sp.stop();
|
|
896
995
|
}
|
|
897
996
|
catch (err) {
|
|
898
|
-
|
|
997
|
+
sp.stop();
|
|
899
998
|
if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
|
|
900
999
|
console.log(chalk.red('\n ✗ Sesión Qwen expirada.'));
|
|
901
1000
|
console.log(chalk.yellow(' Ejecutá: agent-mp --login (o agent-explorer --login)\n'));
|
package/dist/ui/input.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export declare class FixedInput {
|
|
|
6
6
|
private _pasting;
|
|
7
7
|
private _pasteAccum;
|
|
8
8
|
private _drawPending;
|
|
9
|
-
private
|
|
9
|
+
private _activityHeader;
|
|
10
|
+
private _activityLines;
|
|
10
11
|
private get rows();
|
|
11
12
|
get cols(): number;
|
|
12
13
|
private get scrollBottom();
|
|
@@ -14,21 +15,26 @@ export declare class FixedInput {
|
|
|
14
15
|
setup(): void;
|
|
15
16
|
teardown(): void;
|
|
16
17
|
redrawBox(): void;
|
|
17
|
-
/** Show or clear the status / spinner line above the input box. */
|
|
18
|
-
setStatus(text: string | null): void;
|
|
19
18
|
suspend(): () => void;
|
|
19
|
+
/** Enter activity mode: show the 5-line log box instead of the input box. */
|
|
20
|
+
startActivity(header: string): void;
|
|
21
|
+
/** Update the header line (spinner frame + elapsed time) without clearing lines. */
|
|
22
|
+
updateActivityHeader(header: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Append a line to the activity log (keeps last ACTIVITY_LINES lines).
|
|
25
|
+
* Strips ANSI codes and skips blank or pure-JSON lines.
|
|
26
|
+
*/
|
|
27
|
+
pushActivity(rawLine: string): void;
|
|
28
|
+
/** Leave activity mode and restore the normal input box. */
|
|
29
|
+
stopActivity(): void;
|
|
20
30
|
readLine(): Promise<string>;
|
|
21
31
|
println(text: string): void;
|
|
22
32
|
printSeparator(): void;
|
|
23
|
-
/** Repaint the status row (between scroll region and input box). */
|
|
24
|
-
private _drawStatusRow;
|
|
25
|
-
/** Debounced draw: coalesces rapid calls (e.g. during paste) into a single repaint. */
|
|
26
33
|
private _scheduleDraw;
|
|
27
|
-
/** Set DECSTBM once — only called in setup() and on resize, never during typing. */
|
|
28
34
|
private _setScrollRegion;
|
|
29
|
-
/** Blank every row in the reserved area. */
|
|
30
35
|
private _clearReserved;
|
|
31
36
|
private _drawBox;
|
|
32
|
-
|
|
37
|
+
private _drawActivityBox;
|
|
38
|
+
private _drawInputBox;
|
|
33
39
|
private _wrapText;
|
|
34
40
|
}
|
package/dist/ui/input.js
CHANGED
|
@@ -6,29 +6,27 @@ const PREFIX = T('│') + B(' > ');
|
|
|
6
6
|
const PREFIX_CONT = T('│') + B(' '); // continuation lines
|
|
7
7
|
const PREFIX_COLS = 4; // visual width of "│ > " and "│ "
|
|
8
8
|
// Maximum content rows the box can grow to (Shift+Enter / word-wrap).
|
|
9
|
-
//
|
|
9
|
+
// RESERVED_ROWS covers the activity box (7 rows) and the input box (max 6 rows + 1 status).
|
|
10
|
+
// Activity box: 1 top border + 5 content lines + 1 bottom border = 7 rows.
|
|
11
|
+
// Input box: 1 status row + up to 4 content + 2 borders = 7 rows.
|
|
10
12
|
const MAX_CONTENT_ROWS = 4;
|
|
11
|
-
const
|
|
13
|
+
const ACTIVITY_LINES = 5;
|
|
14
|
+
const RESERVED_ROWS = MAX_CONTENT_ROWS + 3; // 7
|
|
12
15
|
// ─── FixedInput ──────────────────────────────────────────────────────────────
|
|
13
|
-
// Keeps an input box pinned to the physical bottom of the terminal.
|
|
14
|
-
// The box starts as 3 rows (border + 1 content + border) and grows up to
|
|
15
|
-
// RESERVED_ROWS when the user types multiline text (Shift+Enter) or the
|
|
16
|
-
// text wraps. The scroll region is set ONCE at setup (and on resize) to
|
|
17
|
-
// [1 .. rows-RESERVED_ROWS] so DECSTBM never fires during normal typing.
|
|
18
16
|
export class FixedInput {
|
|
19
17
|
buf = '';
|
|
20
18
|
history = [];
|
|
21
19
|
histIdx = -1;
|
|
22
20
|
origLog;
|
|
23
|
-
_pasting = false;
|
|
24
|
-
_pasteAccum = '';
|
|
25
|
-
_drawPending = false;
|
|
26
|
-
|
|
21
|
+
_pasting = false;
|
|
22
|
+
_pasteAccum = '';
|
|
23
|
+
_drawPending = false;
|
|
24
|
+
// ── Activity box state (null = input mode, string = activity mode) ──────────
|
|
25
|
+
_activityHeader = null;
|
|
26
|
+
_activityLines = [];
|
|
27
27
|
get rows() { return process.stdout.rows || 24; }
|
|
28
28
|
get cols() { return process.stdout.columns || 80; }
|
|
29
|
-
// The scroll region always ends here — everything below is reserved for the box.
|
|
30
29
|
get scrollBottom() { return this.rows - RESERVED_ROWS; }
|
|
31
|
-
// How many content rows the current buffer needs (1 .. MAX_CONTENT_ROWS).
|
|
32
30
|
_contentRows() {
|
|
33
31
|
const w = this.cols - PREFIX_COLS - 2;
|
|
34
32
|
if (w <= 0)
|
|
@@ -51,7 +49,7 @@ export class FixedInput {
|
|
|
51
49
|
process.stdout.write(`\x1b[${this.scrollBottom};1H`);
|
|
52
50
|
this._clearReserved();
|
|
53
51
|
this._drawBox();
|
|
54
|
-
process.stdout.write('\x1b[?2004h');
|
|
52
|
+
process.stdout.write('\x1b[?2004h');
|
|
55
53
|
process.stdout.on('resize', () => {
|
|
56
54
|
this._setScrollRegion();
|
|
57
55
|
this._clearReserved();
|
|
@@ -59,21 +57,18 @@ export class FixedInput {
|
|
|
59
57
|
});
|
|
60
58
|
}
|
|
61
59
|
teardown() {
|
|
60
|
+
this._activityHeader = null;
|
|
61
|
+
this._activityLines = [];
|
|
62
62
|
console.log = this.origLog;
|
|
63
|
-
process.stdout.write('\x1b[?2004l');
|
|
64
|
-
process.stdout.write('\x1b[r');
|
|
65
|
-
process.stdout.write('\x1b[?25h');
|
|
63
|
+
process.stdout.write('\x1b[?2004l');
|
|
64
|
+
process.stdout.write('\x1b[r');
|
|
65
|
+
process.stdout.write('\x1b[?25h');
|
|
66
66
|
process.stdout.write(`\x1b[${this.rows};1H\n`);
|
|
67
67
|
}
|
|
68
68
|
redrawBox() { this._drawBox(); }
|
|
69
|
-
/** Show or clear the status / spinner line above the input box. */
|
|
70
|
-
setStatus(text) {
|
|
71
|
-
this._statusText = text || '';
|
|
72
|
-
this._drawBox();
|
|
73
|
-
}
|
|
74
69
|
suspend() {
|
|
75
70
|
console.log = this.origLog;
|
|
76
|
-
process.stdout.write('\x1b[?2004l');
|
|
71
|
+
process.stdout.write('\x1b[?2004l');
|
|
77
72
|
process.stdout.write('\x1b[r');
|
|
78
73
|
this._clearReserved();
|
|
79
74
|
process.stdout.write(`\x1b[${this.scrollBottom};1H`);
|
|
@@ -85,9 +80,46 @@ export class FixedInput {
|
|
|
85
80
|
this._setScrollRegion();
|
|
86
81
|
this._clearReserved();
|
|
87
82
|
this._drawBox();
|
|
88
|
-
process.stdout.write('\x1b[?2004h');
|
|
83
|
+
process.stdout.write('\x1b[?2004h');
|
|
89
84
|
};
|
|
90
85
|
}
|
|
86
|
+
// ── Activity box API ───────────────────────────────────────────────────────
|
|
87
|
+
/** Enter activity mode: show the 5-line log box instead of the input box. */
|
|
88
|
+
startActivity(header) {
|
|
89
|
+
this._activityHeader = header;
|
|
90
|
+
this._activityLines = [];
|
|
91
|
+
this._drawBox();
|
|
92
|
+
}
|
|
93
|
+
/** Update the header line (spinner frame + elapsed time) without clearing lines. */
|
|
94
|
+
updateActivityHeader(header) {
|
|
95
|
+
this._activityHeader = header;
|
|
96
|
+
this._drawBox();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Append a line to the activity log (keeps last ACTIVITY_LINES lines).
|
|
100
|
+
* Strips ANSI codes and skips blank or pure-JSON lines.
|
|
101
|
+
*/
|
|
102
|
+
pushActivity(rawLine) {
|
|
103
|
+
if (this._activityHeader === null)
|
|
104
|
+
return;
|
|
105
|
+
// Strip ANSI escape sequences
|
|
106
|
+
const clean = rawLine
|
|
107
|
+
.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '')
|
|
108
|
+
.replace(/[^\x20-\x7e\u00a0-\uffff]/g, '')
|
|
109
|
+
.trim();
|
|
110
|
+
if (!clean)
|
|
111
|
+
return;
|
|
112
|
+
this._activityLines.push(clean);
|
|
113
|
+
if (this._activityLines.length > ACTIVITY_LINES)
|
|
114
|
+
this._activityLines.shift();
|
|
115
|
+
this._scheduleDraw();
|
|
116
|
+
}
|
|
117
|
+
/** Leave activity mode and restore the normal input box. */
|
|
118
|
+
stopActivity() {
|
|
119
|
+
this._activityHeader = null;
|
|
120
|
+
this._activityLines = [];
|
|
121
|
+
this._drawBox();
|
|
122
|
+
}
|
|
91
123
|
// ── Input ──────────────────────────────────────────────────────────────────
|
|
92
124
|
readLine() {
|
|
93
125
|
this.buf = '';
|
|
@@ -111,7 +143,6 @@ export class FixedInput {
|
|
|
111
143
|
if (key.includes('\x1b[200~')) {
|
|
112
144
|
this._pasting = true;
|
|
113
145
|
this._pasteAccum = '';
|
|
114
|
-
// Strip the start marker and handle any content after it in the same chunk
|
|
115
146
|
const after = key.slice(key.indexOf('\x1b[200~') + 6);
|
|
116
147
|
if (after)
|
|
117
148
|
this._pasteAccum += after;
|
|
@@ -120,7 +151,6 @@ export class FixedInput {
|
|
|
120
151
|
// ── Bracketed paste: accumulate ───────────────────────────────
|
|
121
152
|
if (this._pasting) {
|
|
122
153
|
if (key.includes('\x1b[201~')) {
|
|
123
|
-
// End marker — append everything before it, then commit
|
|
124
154
|
const before = key.slice(0, key.indexOf('\x1b[201~'));
|
|
125
155
|
this._pasteAccum += before;
|
|
126
156
|
this.buf += this._pasteAccum;
|
|
@@ -133,17 +163,15 @@ export class FixedInput {
|
|
|
133
163
|
}
|
|
134
164
|
return;
|
|
135
165
|
}
|
|
136
|
-
// ── Shift+Enter →
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
hex === '
|
|
141
|
-
hex === '
|
|
142
|
-
hex === '1b4f4d' // \x1bOM DECNKP
|
|
143
|
-
) {
|
|
166
|
+
// ── Shift+Enter → newline ────────────────────────────────────
|
|
167
|
+
if (hex === '5c0d' ||
|
|
168
|
+
key === '\x0a' ||
|
|
169
|
+
hex === '1b5b31333b327e' ||
|
|
170
|
+
hex === '1b5b31333b3275' ||
|
|
171
|
+
hex === '1b4f4d') {
|
|
144
172
|
this.buf += '\n';
|
|
145
173
|
this._scheduleDraw();
|
|
146
|
-
// ── Enter → submit
|
|
174
|
+
// ── Enter → submit ───────────────────────────────────────────
|
|
147
175
|
}
|
|
148
176
|
else if (key === '\r') {
|
|
149
177
|
const line = this.buf;
|
|
@@ -154,20 +182,20 @@ export class FixedInput {
|
|
|
154
182
|
}
|
|
155
183
|
done(line);
|
|
156
184
|
}
|
|
157
|
-
else if (key === '\x7f' || key === '\x08') {
|
|
185
|
+
else if (key === '\x7f' || key === '\x08') {
|
|
158
186
|
if (this.buf.length > 0) {
|
|
159
187
|
this.buf = this.buf.slice(0, -1);
|
|
160
188
|
this._scheduleDraw();
|
|
161
189
|
}
|
|
162
190
|
}
|
|
163
|
-
else if (key === '\x03') {
|
|
191
|
+
else if (key === '\x03') {
|
|
164
192
|
this.teardown();
|
|
165
193
|
process.exit(0);
|
|
166
194
|
}
|
|
167
|
-
else if (key === '\x04') {
|
|
195
|
+
else if (key === '\x04') {
|
|
168
196
|
done('/exit');
|
|
169
197
|
}
|
|
170
|
-
else if (key === '\x15') {
|
|
198
|
+
else if (key === '\x15') {
|
|
171
199
|
this.buf = '';
|
|
172
200
|
this._scheduleDraw();
|
|
173
201
|
}
|
|
@@ -207,49 +235,60 @@ export class FixedInput {
|
|
|
207
235
|
this.println(chalk.rgb(0, 120, 116)('─'.repeat(this.cols - 1)));
|
|
208
236
|
}
|
|
209
237
|
// ── Private drawing ────────────────────────────────────────────────────────
|
|
210
|
-
/** Repaint the status row (between scroll region and input box). */
|
|
211
|
-
_drawStatusRow() {
|
|
212
|
-
const row = this.scrollBottom + 1;
|
|
213
|
-
process.stdout.write(`\x1b[${row};1H\x1b[2K`);
|
|
214
|
-
if (this._statusText) {
|
|
215
|
-
process.stdout.write(chalk.rgb(0, 185, 180)(this._statusText));
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
/** Debounced draw: coalesces rapid calls (e.g. during paste) into a single repaint. */
|
|
219
238
|
_scheduleDraw() {
|
|
220
239
|
if (this._drawPending)
|
|
221
240
|
return;
|
|
222
241
|
this._drawPending = true;
|
|
223
|
-
setImmediate(() => {
|
|
224
|
-
this._drawPending = false;
|
|
225
|
-
this._drawBox();
|
|
226
|
-
});
|
|
242
|
+
setImmediate(() => { this._drawPending = false; this._drawBox(); });
|
|
227
243
|
}
|
|
228
|
-
/** Set DECSTBM once — only called in setup() and on resize, never during typing. */
|
|
229
244
|
_setScrollRegion() {
|
|
230
245
|
const sb = this.scrollBottom;
|
|
231
246
|
if (sb >= 1)
|
|
232
247
|
process.stdout.write(`\x1b[1;${sb}r`);
|
|
233
248
|
}
|
|
234
|
-
/** Blank every row in the reserved area. */
|
|
235
249
|
_clearReserved() {
|
|
236
250
|
for (let r = this.scrollBottom + 1; r <= this.rows; r++)
|
|
237
251
|
process.stdout.write(`\x1b[${r};1H\x1b[2K`);
|
|
238
252
|
}
|
|
239
253
|
_drawBox() {
|
|
254
|
+
process.stdout.write('\x1b[?25l');
|
|
255
|
+
this._clearReserved();
|
|
256
|
+
if (this._activityHeader !== null) {
|
|
257
|
+
this._drawActivityBox();
|
|
258
|
+
// Cursor stays hidden while agent is working (no input expected)
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this._drawInputBox();
|
|
262
|
+
process.stdout.write('\x1b[?25h');
|
|
263
|
+
}
|
|
264
|
+
// ── Activity box (shown while a subagent is running) ───────────────────────
|
|
265
|
+
_drawActivityBox() {
|
|
266
|
+
const cols = this.cols;
|
|
267
|
+
const inner = cols - 4; // │ + space + content + space + │
|
|
268
|
+
const topRow = this.scrollBottom + 1;
|
|
269
|
+
const header = (this._activityHeader || '').slice(0, cols - 4);
|
|
270
|
+
const dashFill = Math.max(0, cols - 3 - header.length);
|
|
271
|
+
// Top border with header text
|
|
272
|
+
process.stdout.write(`\x1b[${topRow};1H`);
|
|
273
|
+
process.stdout.write(T('╭─') + chalk.bold.white(header) + T('─'.repeat(dashFill)) + T('╮'));
|
|
274
|
+
// Content rows (last ACTIVITY_LINES lines, or blank)
|
|
275
|
+
for (let i = 0; i < ACTIVITY_LINES; i++) {
|
|
276
|
+
const row = topRow + 1 + i;
|
|
277
|
+
const line = (this._activityLines[i] ?? '').slice(0, inner);
|
|
278
|
+
const pad = inner - line.length;
|
|
279
|
+
process.stdout.write(`\x1b[${row};1H`);
|
|
280
|
+
process.stdout.write(T('│') + ' ' + chalk.rgb(180, 210, 210)(line) + ' '.repeat(pad) + ' ' + T('│'));
|
|
281
|
+
}
|
|
282
|
+
// Bottom border
|
|
283
|
+
process.stdout.write(`\x1b[${topRow + ACTIVITY_LINES + 1};1H`);
|
|
284
|
+
process.stdout.write(T('╰') + T('─'.repeat(cols - 2)) + T('╯'));
|
|
285
|
+
}
|
|
286
|
+
// ── Normal input box ───────────────────────────────────────────────────────
|
|
287
|
+
_drawInputBox() {
|
|
240
288
|
const cols = this.cols;
|
|
241
289
|
const cRows = this._contentRows();
|
|
242
290
|
const cWidth = cols - PREFIX_COLS - 2;
|
|
243
|
-
// The box occupies the bottom of the terminal:
|
|
244
|
-
// topBorder = rows - cRows - 1
|
|
245
|
-
// content rows = rows - cRows ... rows - 1
|
|
246
|
-
// bottomBorder = rows
|
|
247
291
|
const topBorder = this.rows - cRows - 1;
|
|
248
|
-
// Hide cursor while repainting
|
|
249
|
-
process.stdout.write('\x1b[?25l');
|
|
250
|
-
// Clear entire reserved area (removes stale content from previous draws)
|
|
251
|
-
this._clearReserved();
|
|
252
|
-
this._drawStatusRow();
|
|
253
292
|
// ── Top border ───────────────────────────────────────────────
|
|
254
293
|
process.stdout.write(`\x1b[${topBorder};1H`);
|
|
255
294
|
process.stdout.write(T('╭') + T('─'.repeat(cols - 2)));
|
|
@@ -261,7 +300,6 @@ export class FixedInput {
|
|
|
261
300
|
for (let i = 0; i < cRows; i++) {
|
|
262
301
|
const row = topBorder + 1 + i;
|
|
263
302
|
let line = visible[i] ?? '';
|
|
264
|
-
// Show overflow indicator when content is clipped above
|
|
265
303
|
if (i === 0 && showStart > 0)
|
|
266
304
|
line = '… ' + line.slice(0, Math.max(0, cWidth - 2));
|
|
267
305
|
else
|
|
@@ -275,18 +313,14 @@ export class FixedInput {
|
|
|
275
313
|
process.stdout.write(`\x1b[${this.rows};1H`);
|
|
276
314
|
process.stdout.write(T('╰') + T('─'.repeat(cols - 2)));
|
|
277
315
|
process.stdout.write(`\x1b[${cols}G` + T('╯'));
|
|
278
|
-
// ── Position cursor
|
|
316
|
+
// ── Position cursor ──────────────────────────────────────────
|
|
279
317
|
const lastLine = visible[visible.length - 1] ?? '';
|
|
280
|
-
const cursorRow = topBorder + cRows;
|
|
318
|
+
const cursorRow = topBorder + cRows;
|
|
281
319
|
const cursorCol = PREFIX_COLS + 1 + lastLine.length;
|
|
282
320
|
process.stdout.write(`\x1b[${cursorRow};${cursorCol}H`);
|
|
283
|
-
process.stdout.write('\x1b[?25h');
|
|
284
321
|
}
|
|
285
|
-
/** Split text into visual lines: split on \n, then wrap each segment. */
|
|
286
322
|
_wrapText(text, maxWidth) {
|
|
287
|
-
if (!text)
|
|
288
|
-
return [''];
|
|
289
|
-
if (maxWidth <= 0)
|
|
323
|
+
if (!text || maxWidth <= 0)
|
|
290
324
|
return [''];
|
|
291
325
|
const result = [];
|
|
292
326
|
for (const seg of text.split('\n')) {
|
|
@@ -15,7 +15,7 @@ export declare function getQwenAccessToken(): Promise<string | null>;
|
|
|
15
15
|
* The qwen CLI manages its own token refresh and uses the correct API format.
|
|
16
16
|
* Falls back to direct HTTP call if the qwen CLI is not available.
|
|
17
17
|
*/
|
|
18
|
-
export declare function callQwenAPI(prompt: string, model?: string): Promise<string>;
|
|
18
|
+
export declare function callQwenAPI(prompt: string, model?: string, onData?: (chunk: string) => void): Promise<string>;
|
|
19
19
|
/**
|
|
20
20
|
* Call Qwen API using credentials from a specific file path (for role binaries).
|
|
21
21
|
* The role binary CLI (e.g. agent-explorer) manages its own qwen auth via the
|
|
@@ -23,4 +23,4 @@ export declare function callQwenAPI(prompt: string, model?: string): Promise<str
|
|
|
23
23
|
* in non-interactive mode without TTY issues.
|
|
24
24
|
* Falls back to direct HTTP if the role binary is not found.
|
|
25
25
|
*/
|
|
26
|
-
export declare function callQwenAPIFromCreds(prompt: string, model: string, credsPath: string): Promise<string>;
|
|
26
|
+
export declare function callQwenAPIFromCreds(prompt: string, model: string, credsPath: string, onData?: (chunk: string) => void): Promise<string>;
|
package/dist/utils/qwen-auth.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as crypto from 'crypto';
|
|
4
|
-
import {
|
|
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
|
+
}
|
|
5
23
|
import open from 'open';
|
|
6
24
|
import { AGENT_HOME } from './config.js';
|
|
7
25
|
const QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai';
|
|
@@ -279,17 +297,12 @@ async function callQwenAPIWithToken(token, prompt, model) {
|
|
|
279
297
|
* The qwen CLI manages its own token refresh and uses the correct API format.
|
|
280
298
|
* Falls back to direct HTTP call if the qwen CLI is not available.
|
|
281
299
|
*/
|
|
282
|
-
export async function callQwenAPI(prompt, model = 'coder-model') {
|
|
300
|
+
export async function callQwenAPI(prompt, model = 'coder-model', onData) {
|
|
283
301
|
// Try using the qwen CLI subprocess first — it handles auth/refresh/format automatically
|
|
284
302
|
const qwenBin = process.env.QWEN_BIN || 'qwen';
|
|
285
303
|
try {
|
|
286
|
-
const result =
|
|
287
|
-
|
|
288
|
-
encoding: 'utf-8',
|
|
289
|
-
timeout: 300000, // 5 minutes
|
|
290
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
291
|
-
});
|
|
292
|
-
if (result.status === 0 && result.stdout?.trim()) {
|
|
304
|
+
const result = await spawnAsync(qwenBin, [], prompt, 300000, onData);
|
|
305
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
293
306
|
return result.stdout.trim();
|
|
294
307
|
}
|
|
295
308
|
// qwen not available or failed — fall through to direct API
|
|
@@ -325,19 +338,14 @@ export async function callQwenAPI(prompt, model = 'coder-model') {
|
|
|
325
338
|
* in non-interactive mode without TTY issues.
|
|
326
339
|
* Falls back to direct HTTP if the role binary is not found.
|
|
327
340
|
*/
|
|
328
|
-
export async function callQwenAPIFromCreds(prompt, model, credsPath) {
|
|
341
|
+
export async function callQwenAPIFromCreds(prompt, model, credsPath, onData) {
|
|
329
342
|
// Derive the role binary name from the creds path (e.g. ~/.agent-explorer/ → agent-explorer)
|
|
330
343
|
const cliName = path.basename(path.dirname(credsPath)).replace(/^\./, '');
|
|
331
|
-
// Try spawning the
|
|
344
|
+
// Try spawning the qwen CLI with piped stdin (async — keeps event loop free)
|
|
332
345
|
const qwenBin = process.env.QWEN_BIN || 'qwen';
|
|
333
346
|
try {
|
|
334
|
-
const result =
|
|
335
|
-
|
|
336
|
-
encoding: 'utf-8',
|
|
337
|
-
timeout: 300000,
|
|
338
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
339
|
-
});
|
|
340
|
-
if (result.status === 0 && result.stdout?.trim()) {
|
|
347
|
+
const result = await spawnAsync(qwenBin, [], prompt, 300000, onData);
|
|
348
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
341
349
|
return result.stdout.trim();
|
|
342
350
|
}
|
|
343
351
|
}
|