agent-mp 0.3.5 → 0.3.7
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/ui/input.d.ts +5 -0
- package/dist/ui/input.js +49 -6
- package/dist/utils/qwen-auth.d.ts +12 -1
- package/dist/utils/qwen-auth.js +52 -5
- package/package.json +1 -1
package/dist/ui/input.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ export declare class FixedInput {
|
|
|
3
3
|
private history;
|
|
4
4
|
private histIdx;
|
|
5
5
|
private origLog;
|
|
6
|
+
private _pasting;
|
|
7
|
+
private _pasteAccum;
|
|
8
|
+
private _drawPending;
|
|
6
9
|
private get rows();
|
|
7
10
|
get cols(): number;
|
|
8
11
|
private get scrollBottom();
|
|
@@ -14,6 +17,8 @@ export declare class FixedInput {
|
|
|
14
17
|
readLine(): Promise<string>;
|
|
15
18
|
println(text: string): void;
|
|
16
19
|
printSeparator(): void;
|
|
20
|
+
/** Debounced draw: coalesces rapid calls (e.g. during paste) into a single repaint. */
|
|
21
|
+
private _scheduleDraw;
|
|
17
22
|
/** Set DECSTBM once — only called in setup() and on resize, never during typing. */
|
|
18
23
|
private _setScrollRegion;
|
|
19
24
|
/** Blank every row in the reserved area. */
|
package/dist/ui/input.js
CHANGED
|
@@ -20,6 +20,9 @@ export class FixedInput {
|
|
|
20
20
|
history = [];
|
|
21
21
|
histIdx = -1;
|
|
22
22
|
origLog;
|
|
23
|
+
_pasting = false; // true while inside bracketed paste sequence
|
|
24
|
+
_pasteAccum = ''; // accumulates paste content between \x1b[200~ and \x1b[201~
|
|
25
|
+
_drawPending = false; // debounce flag
|
|
23
26
|
get rows() { return process.stdout.rows || 24; }
|
|
24
27
|
get cols() { return process.stdout.columns || 80; }
|
|
25
28
|
// The scroll region always ends here — everything below is reserved for the box.
|
|
@@ -47,6 +50,7 @@ export class FixedInput {
|
|
|
47
50
|
process.stdout.write(`\x1b[${this.scrollBottom};1H`);
|
|
48
51
|
this._clearReserved();
|
|
49
52
|
this._drawBox();
|
|
53
|
+
process.stdout.write('\x1b[?2004h'); // enable bracketed paste mode
|
|
50
54
|
process.stdout.on('resize', () => {
|
|
51
55
|
this._setScrollRegion();
|
|
52
56
|
this._clearReserved();
|
|
@@ -55,6 +59,7 @@ export class FixedInput {
|
|
|
55
59
|
}
|
|
56
60
|
teardown() {
|
|
57
61
|
console.log = this.origLog;
|
|
62
|
+
process.stdout.write('\x1b[?2004l'); // disable bracketed paste mode
|
|
58
63
|
process.stdout.write('\x1b[r'); // reset scroll region
|
|
59
64
|
process.stdout.write('\x1b[?25h'); // show cursor
|
|
60
65
|
process.stdout.write(`\x1b[${this.rows};1H\n`);
|
|
@@ -62,6 +67,7 @@ export class FixedInput {
|
|
|
62
67
|
redrawBox() { this._drawBox(); }
|
|
63
68
|
suspend() {
|
|
64
69
|
console.log = this.origLog;
|
|
70
|
+
process.stdout.write('\x1b[?2004l'); // disable bracketed paste while suspended
|
|
65
71
|
process.stdout.write('\x1b[r');
|
|
66
72
|
this._clearReserved();
|
|
67
73
|
process.stdout.write(`\x1b[${this.scrollBottom};1H`);
|
|
@@ -73,6 +79,7 @@ export class FixedInput {
|
|
|
73
79
|
this._setScrollRegion();
|
|
74
80
|
this._clearReserved();
|
|
75
81
|
this._drawBox();
|
|
82
|
+
process.stdout.write('\x1b[?2004h'); // re-enable bracketed paste mode
|
|
76
83
|
};
|
|
77
84
|
}
|
|
78
85
|
// ── Input ──────────────────────────────────────────────────────────────────
|
|
@@ -94,6 +101,32 @@ export class FixedInput {
|
|
|
94
101
|
const onData = (data) => {
|
|
95
102
|
const hex = data.toString('hex');
|
|
96
103
|
const key = data.toString();
|
|
104
|
+
// ── Bracketed paste: start ────────────────────────────────────
|
|
105
|
+
if (key.includes('\x1b[200~')) {
|
|
106
|
+
this._pasting = true;
|
|
107
|
+
this._pasteAccum = '';
|
|
108
|
+
// Strip the start marker and handle any content after it in the same chunk
|
|
109
|
+
const after = key.slice(key.indexOf('\x1b[200~') + 6);
|
|
110
|
+
if (after)
|
|
111
|
+
this._pasteAccum += after;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// ── Bracketed paste: accumulate ───────────────────────────────
|
|
115
|
+
if (this._pasting) {
|
|
116
|
+
if (key.includes('\x1b[201~')) {
|
|
117
|
+
// End marker — append everything before it, then commit
|
|
118
|
+
const before = key.slice(0, key.indexOf('\x1b[201~'));
|
|
119
|
+
this._pasteAccum += before;
|
|
120
|
+
this.buf += this._pasteAccum;
|
|
121
|
+
this._pasting = false;
|
|
122
|
+
this._pasteAccum = '';
|
|
123
|
+
this._scheduleDraw();
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this._pasteAccum += key;
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
97
130
|
// ── Shift+Enter → insert newline into buffer ──────────────────
|
|
98
131
|
// Different terminals send different sequences:
|
|
99
132
|
if (hex === '5c0d' || // \\\r (GNOME Terminal, ThinkPad, many Linux)
|
|
@@ -103,7 +136,7 @@ export class FixedInput {
|
|
|
103
136
|
hex === '1b4f4d' // \x1bOM DECNKP
|
|
104
137
|
) {
|
|
105
138
|
this.buf += '\n';
|
|
106
|
-
this.
|
|
139
|
+
this._scheduleDraw();
|
|
107
140
|
// ── Enter → submit ────────────────────────────────────────────
|
|
108
141
|
}
|
|
109
142
|
else if (key === '\r') {
|
|
@@ -118,7 +151,7 @@ export class FixedInput {
|
|
|
118
151
|
else if (key === '\x7f' || key === '\x08') { // Backspace
|
|
119
152
|
if (this.buf.length > 0) {
|
|
120
153
|
this.buf = this.buf.slice(0, -1);
|
|
121
|
-
this.
|
|
154
|
+
this._scheduleDraw();
|
|
122
155
|
}
|
|
123
156
|
}
|
|
124
157
|
else if (key === '\x03') { // Ctrl+C
|
|
@@ -130,13 +163,13 @@ export class FixedInput {
|
|
|
130
163
|
}
|
|
131
164
|
else if (key === '\x15') { // Ctrl+U
|
|
132
165
|
this.buf = '';
|
|
133
|
-
this.
|
|
166
|
+
this._scheduleDraw();
|
|
134
167
|
}
|
|
135
168
|
else if (hex === '1b5b41') { // Arrow ↑
|
|
136
169
|
if (this.histIdx + 1 < this.history.length) {
|
|
137
170
|
this.histIdx++;
|
|
138
171
|
this.buf = this.history[this.histIdx];
|
|
139
|
-
this.
|
|
172
|
+
this._scheduleDraw();
|
|
140
173
|
}
|
|
141
174
|
}
|
|
142
175
|
else if (hex === '1b5b42') { // Arrow ↓
|
|
@@ -148,11 +181,11 @@ export class FixedInput {
|
|
|
148
181
|
this.histIdx = -1;
|
|
149
182
|
this.buf = '';
|
|
150
183
|
}
|
|
151
|
-
this.
|
|
184
|
+
this._scheduleDraw();
|
|
152
185
|
}
|
|
153
186
|
else if (key.length >= 1 && key.charCodeAt(0) >= 32 && !key.startsWith('\x1b')) {
|
|
154
187
|
this.buf += key;
|
|
155
|
-
this.
|
|
188
|
+
this._scheduleDraw();
|
|
156
189
|
}
|
|
157
190
|
};
|
|
158
191
|
process.stdin.on('data', onData);
|
|
@@ -168,6 +201,16 @@ export class FixedInput {
|
|
|
168
201
|
this.println(chalk.rgb(0, 120, 116)('─'.repeat(this.cols - 1)));
|
|
169
202
|
}
|
|
170
203
|
// ── Private drawing ────────────────────────────────────────────────────────
|
|
204
|
+
/** Debounced draw: coalesces rapid calls (e.g. during paste) into a single repaint. */
|
|
205
|
+
_scheduleDraw() {
|
|
206
|
+
if (this._drawPending)
|
|
207
|
+
return;
|
|
208
|
+
this._drawPending = true;
|
|
209
|
+
setImmediate(() => {
|
|
210
|
+
this._drawPending = false;
|
|
211
|
+
this._drawBox();
|
|
212
|
+
});
|
|
213
|
+
}
|
|
171
214
|
/** Set DECSTBM once — only called in setup() and on resize, never during typing. */
|
|
172
215
|
_setScrollRegion() {
|
|
173
216
|
const sb = this.scrollBottom;
|
|
@@ -10,6 +10,17 @@ export declare function qwenAuthStatus(): Promise<{
|
|
|
10
10
|
}>;
|
|
11
11
|
export declare function fetchQwenModels(): Promise<string[]>;
|
|
12
12
|
export declare function getQwenAccessToken(): Promise<string | null>;
|
|
13
|
+
/**
|
|
14
|
+
* Call Qwen by spawning the `qwen` CLI with piped stdin.
|
|
15
|
+
* The qwen CLI manages its own token refresh and uses the correct API format.
|
|
16
|
+
* Falls back to direct HTTP call if the qwen CLI is not available.
|
|
17
|
+
*/
|
|
13
18
|
export declare function callQwenAPI(prompt: string, model?: string): Promise<string>;
|
|
14
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Call Qwen API using credentials from a specific file path (for role binaries).
|
|
21
|
+
* The role binary CLI (e.g. agent-explorer) manages its own qwen auth via the
|
|
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.
|
|
25
|
+
*/
|
|
15
26
|
export declare function callQwenAPIFromCreds(prompt: string, model: string, credsPath: string): Promise<string>;
|
package/dist/utils/qwen-auth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as crypto from 'crypto';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
4
5
|
import open from 'open';
|
|
5
6
|
import { AGENT_HOME } from './config.js';
|
|
6
7
|
const QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai';
|
|
@@ -273,7 +274,30 @@ async function callQwenAPIWithToken(token, prompt, model) {
|
|
|
273
274
|
const data = await response.json();
|
|
274
275
|
return data.choices?.[0]?.message?.content || '';
|
|
275
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Call Qwen by spawning the `qwen` CLI with piped stdin.
|
|
279
|
+
* The qwen CLI manages its own token refresh and uses the correct API format.
|
|
280
|
+
* Falls back to direct HTTP call if the qwen CLI is not available.
|
|
281
|
+
*/
|
|
276
282
|
export async function callQwenAPI(prompt, model = 'coder-model') {
|
|
283
|
+
// Try using the qwen CLI subprocess first — it handles auth/refresh/format automatically
|
|
284
|
+
const qwenBin = process.env.QWEN_BIN || 'qwen';
|
|
285
|
+
try {
|
|
286
|
+
const result = spawnSync(qwenBin, [], {
|
|
287
|
+
input: prompt,
|
|
288
|
+
encoding: 'utf-8',
|
|
289
|
+
timeout: 300000, // 5 minutes
|
|
290
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
291
|
+
});
|
|
292
|
+
if (result.status === 0 && result.stdout?.trim()) {
|
|
293
|
+
return result.stdout.trim();
|
|
294
|
+
}
|
|
295
|
+
// qwen not available or failed — fall through to direct API
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// qwen not installed — fall through
|
|
299
|
+
}
|
|
300
|
+
// Fallback: direct API call (requires valid token in AGENT_HOME)
|
|
277
301
|
let token = await loadToken();
|
|
278
302
|
if (!token) {
|
|
279
303
|
throw new Error('QWEN_AUTH_EXPIRED: No hay token de Qwen. Ejecutá --login primero.');
|
|
@@ -284,7 +308,6 @@ export async function callQwenAPI(prompt, model = 'coder-model') {
|
|
|
284
308
|
catch (err) {
|
|
285
309
|
if (!err.message?.startsWith('QWEN_AUTH_EXPIRED'))
|
|
286
310
|
throw err;
|
|
287
|
-
// 401 — intentar refresh antes de rendirse
|
|
288
311
|
if (token.refreshToken) {
|
|
289
312
|
const refreshed = await doRefreshToken(token.refreshToken);
|
|
290
313
|
if (refreshed) {
|
|
@@ -295,14 +318,39 @@ export async function callQwenAPI(prompt, model = 'coder-model') {
|
|
|
295
318
|
throw new Error('QWEN_AUTH_EXPIRED: Sesión expirada. Ejecutá: agent-mp --login');
|
|
296
319
|
}
|
|
297
320
|
}
|
|
298
|
-
/**
|
|
321
|
+
/**
|
|
322
|
+
* Call Qwen API using credentials from a specific file path (for role binaries).
|
|
323
|
+
* The role binary CLI (e.g. agent-explorer) manages its own qwen auth via the
|
|
324
|
+
* shared ~/.qwen/oauth_creds.json — we spawn it with piped stdin so it runs
|
|
325
|
+
* in non-interactive mode without TTY issues.
|
|
326
|
+
* Falls back to direct HTTP if the role binary is not found.
|
|
327
|
+
*/
|
|
299
328
|
export async function callQwenAPIFromCreds(prompt, model, credsPath) {
|
|
329
|
+
// Derive the role binary name from the creds path (e.g. ~/.agent-explorer/ → agent-explorer)
|
|
330
|
+
const cliName = path.basename(path.dirname(credsPath)).replace(/^\./, '');
|
|
331
|
+
// Try spawning the role binary with piped stdin (non-interactive mode)
|
|
332
|
+
const qwenBin = process.env.QWEN_BIN || 'qwen';
|
|
333
|
+
try {
|
|
334
|
+
const result = spawnSync(qwenBin, [], {
|
|
335
|
+
input: prompt,
|
|
336
|
+
encoding: 'utf-8',
|
|
337
|
+
timeout: 300000,
|
|
338
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
339
|
+
});
|
|
340
|
+
if (result.status === 0 && result.stdout?.trim()) {
|
|
341
|
+
return result.stdout.trim();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// qwen not available
|
|
346
|
+
}
|
|
347
|
+
// Fallback: direct HTTP with stored creds
|
|
300
348
|
let raw;
|
|
301
349
|
try {
|
|
302
350
|
raw = JSON.parse(await fs.readFile(credsPath, 'utf-8'));
|
|
303
351
|
}
|
|
304
352
|
catch {
|
|
305
|
-
throw new Error(`No credentials found at ${credsPath}. Run
|
|
353
|
+
throw new Error(`No credentials found at ${credsPath}. Run: ${cliName} --login`);
|
|
306
354
|
}
|
|
307
355
|
let token = {
|
|
308
356
|
accessToken: raw.accessToken || raw.access_token || '',
|
|
@@ -312,7 +360,7 @@ export async function callQwenAPIFromCreds(prompt, model, credsPath) {
|
|
|
312
360
|
resourceUrl: raw.resourceUrl || raw.resource_url,
|
|
313
361
|
};
|
|
314
362
|
if (!token.accessToken) {
|
|
315
|
-
throw new Error(`Invalid credentials at ${credsPath}. Run
|
|
363
|
+
throw new Error(`Invalid credentials at ${credsPath}. Run: ${cliName} --login`);
|
|
316
364
|
}
|
|
317
365
|
// Refresh proactivo: si vence en menos de 2 minutos (o ya venció)
|
|
318
366
|
const TWO_MIN = 2 * 60 * 1000;
|
|
@@ -337,7 +385,6 @@ export async function callQwenAPIFromCreds(prompt, model, credsPath) {
|
|
|
337
385
|
return callQwenAPIWithToken(refreshed, prompt, model);
|
|
338
386
|
}
|
|
339
387
|
}
|
|
340
|
-
const cliName = path.basename(path.dirname(credsPath)).replace(/^\./, '');
|
|
341
388
|
throw new Error(`QWEN_AUTH_EXPIRED: Sesión expirada. Ejecutá: ${cliName} --login`);
|
|
342
389
|
}
|
|
343
390
|
}
|