agent-mp 0.5.7 → 0.5.9

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.
@@ -683,6 +683,57 @@ async function cmdAuthStatus(fi) {
683
683
  }));
684
684
  fi.println(renderSectionBox('Auth Status', rows));
685
685
  }
686
+ async function cmdUsage(fi) {
687
+ const { qwenUsage } = await import('../utils/qwen-auth.js');
688
+ const { PKG_NAME, getConfigDir } = await import('../utils/config.js');
689
+ const { loadProjectConfig } = await import('../utils/config.js');
690
+ fi.println(chalk.bold.cyan('\n Uso / Quota\n'));
691
+ // Check each role CLI
692
+ const dir = await resolveProjectDir(process.cwd());
693
+ let config;
694
+ try {
695
+ config = await loadProjectConfig(dir);
696
+ }
697
+ catch {
698
+ config = null;
699
+ }
700
+ const checks = [];
701
+ // Check main PKG_NAME (this CLI's own credentials)
702
+ const mainResult = await qwenUsage();
703
+ const mainLabel = `${PKG_NAME} (${mainResult.token ? 'coder-model' : '?'})`;
704
+ if (mainResult.ok) {
705
+ const exp = mainResult.token ? new Date(mainResult.token.expiresAt).toLocaleTimeString() : '?';
706
+ checks.push({ key: mainLabel, value: chalk.green(`✓ quota OK — expires ${exp}`) });
707
+ }
708
+ else {
709
+ checks.push({ key: mainLabel, value: chalk.red(`✗ ${mainResult.error}`) });
710
+ }
711
+ // Check each configured role CLI
712
+ if (config?.roles) {
713
+ for (const [roleName, roleCfg] of Object.entries(config.roles)) {
714
+ const r = roleCfg;
715
+ if (!r?.cli)
716
+ continue;
717
+ const homeDir = path.join(os.homedir(), '.' + r.cli);
718
+ const credsPath = path.join(homeDir, 'oauth_creds.json');
719
+ if (await fileExists(credsPath)) {
720
+ const result = await qwenUsage(credsPath);
721
+ const exp = result.token ? new Date(result.token.expiresAt).toLocaleTimeString() : '?';
722
+ if (result.ok) {
723
+ checks.push({ key: `${r.cli} (${roleName})`, value: chalk.green(`✓ quota OK — expires ${exp}`) });
724
+ }
725
+ else {
726
+ checks.push({ key: `${r.cli} (${roleName})`, value: chalk.red(`✗ ${result.error}`) });
727
+ }
728
+ }
729
+ else {
730
+ checks.push({ key: `${r.cli} (${roleName})`, value: chalk.yellow('not logged in') });
731
+ }
732
+ }
733
+ }
734
+ fi.println(renderSectionBox('Quota Status', checks));
735
+ fi.println('');
736
+ }
686
737
  function cmdHelp(fi) {
687
738
  const commands = [
688
739
  { key: '/setup', value: 'Full interactive setup wizard' },
@@ -703,6 +754,7 @@ function cmdHelp(fi) {
703
754
  { key: '/login', value: 'Login (Qwen OAuth or CLI auth)' },
704
755
  { key: '/logout', value: 'Logout and clear credentials' },
705
756
  { key: '/auth-status', value: 'Show authentication status' },
757
+ { key: '/usage', value: 'Check quota status for all role CLIs' },
706
758
  { key: '/tasks', value: 'List all tasks' },
707
759
  { key: '/clear', value: 'Clear the screen' },
708
760
  { key: '/help', value: 'Show this help' },
@@ -1067,6 +1119,9 @@ export async function runRepl(resumeSession) {
1067
1119
  case 'auth-status':
1068
1120
  await cmdAuthStatus(fi);
1069
1121
  break;
1122
+ case 'usage':
1123
+ await cmdUsage(fi);
1124
+ break;
1070
1125
  case 'tasks':
1071
1126
  await cmdTasks(fi);
1072
1127
  break;
@@ -985,27 +985,32 @@ INSTRUCCIONES:
985
985
  let fsSnapshot = '';
986
986
  try {
987
987
  const { execSync } = await import('child_process');
988
- // Use find but exclude .agent, node_modules, .git, dist, __pycache__, .venv, target, build
989
988
  fsSnapshot = execSync(`find "${this.projectDir}" -maxdepth 3 -not -path '*/.agent/*' -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/__pycache__/*' -not -path '*/.venv/*' -not -path '*/target/*' -not -path '*/build/*' -not -name '.git' | sort | head -300`, { encoding: 'utf-8', timeout: 10000 });
990
989
  }
991
990
  catch { /* ignore */ }
992
- // Read existing architecture docs if any
993
- const archPath = path.join(contextDir, 'architecture.md');
994
- let existingArch = '';
991
+ // Read existing main architecture doc
992
+ const mainArchPath = path.join(contextDir, 'architecture.md');
993
+ let existingMainArch = '';
995
994
  try {
996
- existingArch = await readFile(archPath);
995
+ existingMainArch = await readFile(mainArchPath);
997
996
  }
998
- catch { /* new project */ }
999
- // Read existing module docs if any
1000
- const modulesDir = path.join(contextDir, 'modules');
1001
- let existingModules = '';
997
+ catch { /* new */ }
998
+ // Read existing per-component docs
999
+ let existingComponentDocs = '';
1002
1000
  try {
1003
- const modFiles = await fs.readdir(modulesDir);
1004
- for (const f of modFiles.filter(f => f.endsWith('.md'))) {
1005
- existingModules += `\n--- ${f} ---\n${await readFile(path.join(modulesDir, f)).then(c => c.slice(0, 2000))}\n`;
1001
+ const ctxEntries = await fs.readdir(contextDir);
1002
+ for (const entry of ctxEntries.filter(e => e !== 'architecture.md' && e !== 'explorer-last.md' && e !== 'modules')) {
1003
+ const entryPath = path.join(contextDir, entry);
1004
+ const stat = await fs.stat(entryPath);
1005
+ if (stat.isDirectory()) {
1006
+ const compArch = path.join(entryPath, 'architecture.md');
1007
+ if (await fileExists(compArch)) {
1008
+ existingComponentDocs += `\n=== EXISTING: ${entry}/architecture.md ===\n${(await readFile(compArch)).slice(0, 1500)}\n`;
1009
+ }
1010
+ }
1006
1011
  }
1007
1012
  }
1008
- catch { /* no modules yet */ }
1013
+ catch { /* ignore */ }
1009
1014
  const effectiveTask = task || 'Explorar y documentar todas las aplicaciones y servicios del proyecto';
1010
1015
  const prompt = `TAREA DE EXPLORACION: ${effectiveTask}
1011
1016
  DIRECTORIO_TRABAJO: ${this.projectDir}
@@ -1015,114 +1020,124 @@ STACK: ${this.config.stack}
1015
1020
  ESTRUCTURA DE ARCHIVOS DEL PROYECTO (excluyendo .agent, node_modules, .git, dist, __pycache__, .venv, target, build):
1016
1021
  ${fsSnapshot}
1017
1022
 
1018
- ${existingArch ? `--- DOCUMENTACION EXISTENTE: architecture.md ---\n${existingArch.slice(0, 2000)}\n` : ''}
1019
- ${existingModules ? `--- DOCUMENTACION EXISTENTE: modules/ ---\n${existingModules.slice(0, 3000)}\n` : ''}
1023
+ ${existingMainArch ? `=== DOCUMENTACION EXISTENTE: architecture.md (principal) ===\n${existingMainArch.slice(0, 2000)}\n` : ''}
1024
+ ${existingComponentDocs ? `=== DOCUMENTACION EXISTENTE por componente ===\n${existingComponentDocs.slice(0, 3000)}\n` : ''}
1020
1025
 
1021
1026
  INSTRUCCIONES:
1022
- Analiza la estructura de archivos y genera documentación COMPLETA y ESCALONADA del proyecto.
1023
- Para cada directorio/modulo debes documentar: QUE ES, COMO SE ESTRUCTURA, QUE NECESITA PARA LEVANTAR, y QUE COMANDOS PUEDE EJECUTAR.
1027
+ Analiza la estructura de archivos y genera documentación ESCALONADA en DOS NIVELES:
1028
+
1029
+ NIVEL 1: architecture.md (raiz de .agent/context/)
1030
+ - Lista TODOS los directorios al nivel del proyecto
1031
+ - Indica si son independientes o conectados entre si (ej: front consume back, dos APIs se consumen entre si)
1032
+ - Prerequisitos generales para levantar el proyecto
1033
+ - Comandos globales de desarrollo
1024
1034
 
1025
- IMPORTANTE: Si un directorio contiene una API o servicio con multiples modulos (rutas, controllers, models, etc.),
1026
- ese directorio debe tener SU PROPIA subcarpeta dentro de modules/ con un index.md y un archivo por cada modulo interno.
1035
+ NIVEL 2: Para CADA directorio que sea un servicio/API/app/funcionalidad:
1036
+ - Crea subcarpeta: [nombre-del-directorio]/architecture.md explica como se estructura internamente
1037
+ - Crea subcarpeta: [nombre-del-directorio]/modules/[modulo].md → detalle de cada modulo interno
1038
+ - Si un directorio es trivial (solo scripts, docs, configs), NO crees subcarpeta, solo mencionarlo en el architecture.md principal
1027
1039
 
1028
- DEVUELVE el contenido de TODOS los archivos a crear en este formato EXACTO (separados por marcadores):
1040
+ FORMATO EXACTO de los archivos a devolver (separados por marcadores):
1029
1041
 
1030
- === ARCHITECTURE.md ===
1031
- # ${this.config.project} — Architecture
1042
+ === architecture.md ===
1043
+ # ${this.config.project} — Architecture Overview
1032
1044
 
1033
- Breve descripcion del proyecto y su stack.
1045
+ Descripcion general del proyecto.
1034
1046
 
1035
- ## Estructura General
1047
+ ## Componentes del Proyecto
1036
1048
 
1037
- Tabla resumen de todos los directorios al nivel del proyecto:
1038
- | Directorio | Tipo | Stack | Proposito | Entry Point |
1049
+ | Componente | Tipo | Stack | Proposito | Entry Point |
1050
+ |------------|------|-------|-----------|-------------|
1051
+ | datamart-data-access-api | API Backend | NestJS | Acceso a datos | src/main.ts |
1052
+ | nexus-core-api | API Backend | NestJS | Core business logic | src/main.ts |
1039
1053
 
1040
- ## Como se relacionan los componentes
1054
+ ## Como se Relacionan los Componentes
1041
1055
 
1042
- Explica como interactuan entre si los directorios/servicios:
1043
- - Dependencias entre componentes
1044
- - Flujo de datos principal
1045
- - Variables de entorno compartidas
1046
- - Bases de datos o servicios externos comunes
1056
+ Explica las conexiones entre componentes:
1057
+ - Si un front consume un back, explicar como
1058
+ - Si dos APIs se consumen entre sí (fetch, HTTP), explicar el flujo
1059
+ - Si son independientes, indicar que cada uno funciona por su cuenta
1060
+
1061
+ ### Diagrama de Flujo (texto)
1062
+ \`\`\`
1063
+ [Frontend] --fetch--> [API 1] --HTTP--> [API 2]
1064
+ [API 1] --> [Database]
1065
+ \`\`\`
1047
1066
 
1048
1067
  ## Prerequisitos Generales
1049
- - Que se necesita instalado globalmente (node, python, java, docker, etc.)
1050
- - Versiones requeridas
1051
- - Servicios externos necesarios (BD, cache, etc.)
1068
+ - Node.js x.x, Python x.x, Docker, etc.
1069
+ - Bases de datos, servicios externos
1052
1070
 
1053
1071
  ## Comandos de Desarrollo Globales
1054
-
1055
1072
  | Comando | Descripcion | Directorio |
1056
1073
  |---------|-------------|------------|
1057
1074
  | ... | ... | ... |
1058
1075
 
1059
- === modules/[nombre]/index.md ===
1060
- # [nombre] — Estructura Interna
1076
+ === [nombre]/architecture.md ===
1077
+ # [nombre] — Arquitectura Interna
1061
1078
 
1062
- Descripcion general de este servicio/API y que problema resuelve.
1079
+ Descripcion de este componente y su rol en el proyecto.
1063
1080
 
1064
- ## Estructura del Servicio
1081
+ ## Estructura Interna
1065
1082
  \`\`\`
1066
1083
  src/
1067
- ├── main.py # entry point
1068
- ├── models/ # modelos de datos
1069
- └── routes/ # endpoints
1084
+ ├── main.ts # entry point
1085
+ ├── config/ # configuracion
1086
+ ├── controllers/ # endpoints
1087
+ └── services/ # logica de negocio
1070
1088
  \`\`\`
1071
1089
 
1072
1090
  ## Modulos Internos
1073
1091
  | Modulo | Archivo | Descripcion |
1074
1092
  |--------|---------|-------------|
1075
- | auth | auth.md | Autenticacion y tokens |
1076
- | users | users.md | Gestion de usuarios |
1093
+ | config | modules/config.md | Configuracion |
1094
+ | controller | modules/controller.md | Endpoints |
1077
1095
 
1078
1096
  ## Como Levantar
1079
- - **Dependencias:** instalar con \`npm install\` o \`pip install -r requirements.txt\`
1097
+ - **Instalar deps:** comando
1080
1098
  - **Servicios necesarios:** BD, redis, etc.
1081
- - **Variables de entorno:** cuales y para que
1082
- - **Comando para iniciar:** \`npm run dev\` o equivalente
1099
+ - **Variables de entorno:** listarlas con su proposito
1100
+ - **Comando para iniciar:** \`...\`
1083
1101
 
1084
1102
  ## Comandos Disponibles
1085
-
1086
1103
  | Comando | Que hace |
1087
1104
  |---------|----------|
1088
1105
  | ... | ... |
1089
1106
 
1090
- === modules/[nombre]/[modulo].md ===
1107
+ ## Comunicacion con Otros Componentes
1108
+ - A que APIs llama o que APIs lo llaman
1109
+ - Protocolo (fetch, HTTP, gRPC, etc.)
1110
+
1111
+ === [nombre]/modules/[modulo].md ===
1091
1112
  # [nombre-del-modulo]
1092
1113
 
1093
1114
  ## Funcion
1094
- Que hace este modulo y que parte del servicio cubre.
1115
+ Que hace este modulo especifico.
1095
1116
 
1096
1117
  ## Archivo Principal
1097
- - **Ruta:** path relativo al proyecto
1098
- - **Funciones/Clases principales:** listar las mas importantes
1118
+ - **Ruta:** path relativo
1119
+ - **Funciones/Clases principales**
1099
1120
 
1100
1121
  ## Endpoints / Interfaces Publicas
1101
- Si es un modulo de API:
1102
- - \`GET /endpoint\` — descripcion
1103
- - \`POST /endpoint\` — descripcion
1104
-
1105
- Si es otro tipo de modulo:
1106
- - Funciones publicas exportadas
1107
- - Interfaces que implementa
1122
+ Si es un controller/router:
1123
+ - \`GET /ruta\` — descripcion
1124
+ - \`POST /ruta\` — descripcion
1108
1125
 
1109
1126
  ## Dependencias
1110
- - **Internas:** otros modulos del mismo servicio que usa
1111
- - **Externas:** librerias de terceros especificas de este modulo
1112
-
1113
- ## Notas
1114
- Cualquier detalle relevante especifico de este modulo.
1127
+ - **Internas:** modulos del mismo componente
1128
+ - **Externas:** librerias de terceros
1115
1129
 
1116
1130
  REGLAS:
1117
- - Devuelve UNICAMENTE los archivos separados por === modules/path/file.md ===
1118
- - NO incluyas explicaciones fuera de los archivos
1119
- - Para CADA API/servicio con modulos internos: crea modules/[nombre]/index.md + un archivo por cada modulo
1120
- - Para directorios triviales (solo docs, configs sueltos): documenta en architecture.md sin crear subcarpeta
1131
+ - Devuelve UNICAMENTE los archivos separados por === path/file.md ===
1132
+ - NO incluyas texto explicativo fuera de los archivos
1133
+ - El archivo principal SIEMPRE es === architecture.md ===
1134
+ - Para cada componente con modulos internos: === [nombre]/architecture.md === + === [nombre]/modules/[modulo].md ===
1135
+ - Para componentes triviales (scripts, docs): solo mencionarlos en architecture.md principal
1121
1136
  - Documenta TODOS los directorios al nivel de DIRECTORIO_TRABAJO (excluyendo .agent)
1122
1137
  - Si hay documentacion existente, actualizala con los cambios detectados`;
1123
1138
  const res = await this.runWithFallback('explorer', prompt, 'Exploracion');
1124
1139
  const text = extractCliText(res);
1125
- // Parse the response: split by === FILENAME.md === markers
1140
+ // Parse sections separated by === path/file.md === markers
1126
1141
  const sections = text.split(/===\s+(.+?\.md)\s*===/).slice(1);
1127
1142
  let filesWritten = 0;
1128
1143
  for (let i = 0; i < sections.length; i += 2) {
@@ -1133,20 +1148,30 @@ REGLAS:
1133
1148
  // Clean up code fences if present
1134
1149
  content = content.replace(/^```markdown\s*/i, '').replace(/^```\s*$/gm, '').trim();
1135
1150
  try {
1136
- if (fileName === 'ARCHITECTURE.md') {
1137
- await writeFile(archPath, content);
1138
- filesWritten++;
1151
+ let targetPath;
1152
+ if (fileName === 'architecture.md' || fileName === 'ARCHITECTURE.md') {
1153
+ // Main architecture doc
1154
+ targetPath = mainArchPath;
1139
1155
  }
1140
- else if (fileName.toLowerCase().includes('modules/')) {
1141
- // Extract the modules/ path from the marker
1142
- const modMatch = fileName.match(/modules\/(.+\.md)/i);
1143
- if (modMatch) {
1144
- const modPath = path.join(contextDir, 'modules', modMatch[1]);
1145
- await fs.mkdir(path.dirname(modPath), { recursive: true });
1146
- await writeFile(modPath, content);
1147
- filesWritten++;
1156
+ else {
1157
+ // Component doc: [component]/architecture.md or [component]/modules/[mod].md
1158
+ // fileName could be: "datamart-data-access-api/architecture.md" or "datamart-data-access-api/modules/config.md"
1159
+ const compMatch = fileName.match(/^(.+?)\/architecture\.md$/i);
1160
+ const modMatch = fileName.match(/^(.+?)\/modules\/(.+\.md)$/i);
1161
+ if (compMatch) {
1162
+ targetPath = path.join(contextDir, compMatch[1], 'architecture.md');
1163
+ }
1164
+ else if (modMatch) {
1165
+ targetPath = path.join(contextDir, modMatch[1], 'modules', modMatch[2]);
1166
+ }
1167
+ else {
1168
+ // Fallback: treat as modules file in legacy modules/ dir
1169
+ targetPath = path.join(contextDir, 'modules', fileName.replace(/.*\//, ''));
1148
1170
  }
1149
1171
  }
1172
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
1173
+ await writeFile(targetPath, content);
1174
+ filesWritten++;
1150
1175
  }
1151
1176
  catch (err) {
1152
1177
  log.warn(`Failed to write ${fileName}: ${err.message}`);
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ program
38
38
  .option('--model [cli]', 'Alias for --models')
39
39
  .option('--reset-coordinator', 'Clear coordinator selection (re-pick on next run)')
40
40
  .option('--reset-auth', 'Wipe all auth credentials and start fresh')
41
+ .option('--usage', 'Check quota status for all role CLIs')
41
42
  .addHelpText('after', `
42
43
  Setup subcommands:
43
44
  agent-mp setup init Full interactive wizard (login + roles + project)
@@ -131,6 +132,22 @@ if (nativeRole) {
131
132
  }
132
133
  process.exit(0);
133
134
  }
135
+ // --usage: check quota status with test API call
136
+ if (args.includes('--usage') || args.includes('usage')) {
137
+ const { qwenUsage, getTokenPath } = await import('./utils/qwen-auth.js');
138
+ const result = await qwenUsage();
139
+ const exp = result.token ? new Date(result.token.expiresAt).toLocaleString() : 'N/A';
140
+ if (result.ok) {
141
+ console.log(chalk.green(` ✓ ${PKG_NAME}: quota OK`));
142
+ console.log(chalk.dim(` Token expires: ${exp}`));
143
+ console.log(chalk.dim(` Model: ${result.model}`));
144
+ }
145
+ else {
146
+ console.log(chalk.red(` ✗ ${PKG_NAME}: ${result.error}`));
147
+ console.log(chalk.dim(` Token expires: ${exp}`));
148
+ }
149
+ process.exit(result.ok ? 0 : 1);
150
+ }
134
151
  // Parse --model flag if provided
135
152
  const modelIdx = args.findIndex(a => a === '--model' || a === '-m');
136
153
  let model;
@@ -150,6 +167,23 @@ if (nativeRole) {
150
167
  }
151
168
  // ─────────────────────────────────────────────────────────────────────────────
152
169
  program.action(async (task, options) => {
170
+ // --usage: check quota status
171
+ if (options.usage) {
172
+ const { qwenUsage } = await import('./utils/qwen-auth.js');
173
+ const { PKG_NAME } = await import('./utils/config.js');
174
+ const result = await qwenUsage();
175
+ const exp = result.token ? new Date(result.token.expiresAt).toLocaleString() : 'N/A';
176
+ if (result.ok) {
177
+ console.log(chalk.green(` ✓ ${PKG_NAME}: quota OK`));
178
+ console.log(chalk.dim(` Token expires: ${exp}`));
179
+ console.log(chalk.dim(` Model: ${result.model}`));
180
+ }
181
+ else {
182
+ console.log(chalk.red(` ✗ ${PKG_NAME}: ${result.error}`));
183
+ console.log(chalk.dim(` Token expires: ${exp}`));
184
+ }
185
+ process.exit(result.ok ? 0 : 1);
186
+ }
153
187
  if (options.resetCoordinator || options.resetAuth) {
154
188
  const authFile = path.join(AGENT_HOME, 'auth.json');
155
189
  const qwenCreds = path.join(AGENT_HOME, 'oauth_creds.json');
@@ -2,6 +2,13 @@
2
2
  export declare const QWEN_AGENT_HOME: string;
3
3
  /** Dynamic getter (recommended for runtime changes) */
4
4
  export declare function getQwenHome(): string;
5
+ interface QwenToken {
6
+ accessToken: string;
7
+ refreshToken: string;
8
+ expiresAt: number;
9
+ idToken?: string;
10
+ resourceUrl?: string;
11
+ }
5
12
  export declare function getTokenPath(): Promise<string>;
6
13
  export declare function qwenLogin(): Promise<boolean>;
7
14
  export declare function qwenAuthStatus(): Promise<{
@@ -20,3 +27,11 @@ export declare function callQwenAPI(prompt: string, model?: string, onData?: (ch
20
27
  * Calls the Qwen REST API directly — no dependency on any qwen CLI binary.
21
28
  */
22
29
  export declare function callQwenAPIFromCreds(prompt: string, model: string, credsPath: string, onData?: (chunk: string) => void): Promise<string>;
30
+ /** Check quota status by making a minimal test API call */
31
+ export declare function qwenUsage(credsPath?: string): Promise<{
32
+ ok: boolean;
33
+ token: QwenToken | null;
34
+ error?: string;
35
+ model?: string;
36
+ }>;
37
+ export {};
@@ -286,9 +286,14 @@ async function callQwenAPIWithToken(token, prompt, model, onData) {
286
286
  });
287
287
  if (!response.ok) {
288
288
  const errorText = await response.text();
289
- if (response.status === 401 || (response.status === 429 && errorText.includes('insufficient_quota'))) {
289
+ // 401 = auth error, refresh may help
290
+ if (response.status === 401) {
290
291
  throw new Error(`QWEN_AUTH_EXPIRED: ${errorText}`);
291
292
  }
293
+ // 429 insufficient_quota = daily quota exhausted, refresh won't help
294
+ if (response.status === 429 && errorText.includes('insufficient_quota')) {
295
+ throw new Error(`QWEN_QUOTA_EXCEEDED: ${errorText}`);
296
+ }
292
297
  throw new Error(`Qwen API error: ${response.status} - ${errorText}`);
293
298
  }
294
299
  if (!useStream) {
@@ -342,6 +347,9 @@ export async function callQwenAPI(prompt, model = 'coder-model', onData) {
342
347
  return await callQwenAPIWithToken(token, prompt, model, onData);
343
348
  }
344
349
  catch (err) {
350
+ // Quota errors: refresh won't help, propagate immediately
351
+ if (err.message?.startsWith('QWEN_QUOTA_EXCEEDED'))
352
+ throw err;
345
353
  if (!err.message?.startsWith('QWEN_AUTH_EXPIRED'))
346
354
  throw err;
347
355
  if (token.refreshToken) {
@@ -390,6 +398,9 @@ export async function callQwenAPIFromCreds(prompt, model, credsPath, onData) {
390
398
  return await callQwenAPIWithToken(token, prompt, model, onData);
391
399
  }
392
400
  catch (err) {
401
+ // Quota errors: refresh won't help, propagate immediately
402
+ if (err.message?.startsWith('QWEN_QUOTA_EXCEEDED'))
403
+ throw err;
393
404
  if (!err.message?.startsWith('QWEN_AUTH_EXPIRED'))
394
405
  throw err;
395
406
  if (token.refreshToken) {
@@ -402,3 +413,58 @@ export async function callQwenAPIFromCreds(prompt, model, credsPath, onData) {
402
413
  throw new Error(`QWEN_AUTH_EXPIRED: Sesión expirada. Ejecutá: ${cliName} --login`);
403
414
  }
404
415
  }
416
+ /** Check quota status by making a minimal test API call */
417
+ export async function qwenUsage(credsPath) {
418
+ let token;
419
+ if (credsPath) {
420
+ try {
421
+ const raw = JSON.parse(await fs.readFile(credsPath, 'utf-8'));
422
+ token = {
423
+ accessToken: raw.accessToken || raw.access_token || '',
424
+ refreshToken: raw.refreshToken || raw.refresh_token || '',
425
+ expiresAt: raw.expiresAt || raw.expiry_date || 0,
426
+ idToken: raw.idToken || raw.id_token,
427
+ resourceUrl: raw.resourceUrl || raw.resource_url,
428
+ };
429
+ }
430
+ catch {
431
+ return { ok: false, token: null, error: 'No credentials found' };
432
+ }
433
+ }
434
+ else {
435
+ token = await loadToken();
436
+ }
437
+ if (!token || !token.accessToken) {
438
+ return { ok: false, token, error: 'No access token — run --login' };
439
+ }
440
+ if (token.expiresAt < Date.now()) {
441
+ // Try refresh
442
+ if (token.refreshToken) {
443
+ const refreshed = await doRefreshToken(token.refreshToken);
444
+ if (refreshed) {
445
+ token = refreshed;
446
+ if (!credsPath)
447
+ await saveToken(refreshed);
448
+ else
449
+ await fs.writeFile(credsPath, JSON.stringify(refreshed, null, 2), 'utf-8');
450
+ }
451
+ else {
452
+ return { ok: false, token, error: 'Token expired and refresh failed' };
453
+ }
454
+ }
455
+ else {
456
+ return { ok: false, token, error: 'Token expired, no refresh token' };
457
+ }
458
+ }
459
+ // Make a minimal test call to check quota
460
+ try {
461
+ const result = await callQwenAPIWithToken(token, 'ping', 'coder-model');
462
+ return { ok: true, token, model: 'coder-model' };
463
+ }
464
+ catch (err) {
465
+ if (err.message?.startsWith('QWEN_QUOTA_EXCEEDED')) {
466
+ return { ok: false, token, error: 'Daily quota exhausted — wait or --login with different account' };
467
+ }
468
+ return { ok: false, token, error: err.message };
469
+ }
470
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-mp",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "description": "Deterministic multi-agent CLI orchestrator — plan, code, review",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",