Skip to content

Commit add6306

Browse files
authored
Merge pull request #5 from victor0602/feat/terminal-ux-improvements
feat: terminal UX improvements — Model hint, quota status & env detection
2 parents 0aaeb48 + bf6ddca commit add6306

7 files changed

Lines changed: 106 additions & 12 deletions

File tree

src/commands/auth/login.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { quotaEndpoint } from '../../client/endpoints';
88
import { formatOutput } from '../../output/formatter';
99
import { getConfigPath } from '../../config/paths';
1010
import { readConfigFile, writeConfigFile } from '../../config/loader';
11+
import { isInteractive } from '../../utils/env';
1112
import type { Config } from '../../config/schema';
1213
import type { GlobalFlags } from '../../types/flags';
1314
import type { CredentialFile } from '../../auth/types';
@@ -29,6 +30,26 @@ export default defineCommand({
2930
'minimax auth login --method api-key --api-key sk-xxxxx',
3031
],
3132
async run(config: Config, flags: GlobalFlags) {
33+
// --- Phase 4: env key detection ---
34+
const envKey = process.env.MINIMAX_API_KEY;
35+
if (envKey) {
36+
const maskedEnvKey = envKey.length > 8 ? `${envKey.slice(0, 4)}...${envKey.slice(-4)}` : '***';
37+
if (isInteractive({ nonInteractive: config.nonInteractive })) {
38+
const { confirm } = await import('@clack/prompts');
39+
const proceed = await confirm({
40+
message: `Detected MINIMAX_API_KEY in environment (${maskedEnvKey}).\nYou are already authenticated via env.\nDo you still want to configure local persistent credentials?`,
41+
initialValue: false,
42+
});
43+
if (!proceed) {
44+
process.stdout.write('Login skipped. Using environment variables.\n');
45+
process.exit(0);
46+
}
47+
} else {
48+
process.stderr.write(`Warning: MINIMAX_API_KEY is already set in environment.\n`);
49+
}
50+
}
51+
// --- Phase 4 end ---
52+
3253
const method = flags.apiKey ? 'api-key' : (flags.method as string) || 'oauth';
3354

3455
if (method === 'api-key') {

src/commands/auth/status.ts

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { defineCommand } from '../../command';
22
import { resolveCredential } from '../../auth/resolver';
33
import { loadCredentials } from '../../auth/credentials';
44
import { formatOutput, detectOutputFormat } from '../../output/formatter';
5+
import { requestJson } from '../../client/http';
6+
import { quotaEndpoint } from '../../client/endpoints';
57
import type { Config } from '../../config/schema';
68
import type { GlobalFlags } from '../../types/flags';
9+
import type { QuotaResponse } from '../../types/api';
710

811
export default defineCommand({
912
name: 'auth status',
10-
description: 'Show current authentication state',
13+
description: 'Show current authentication state and quota snapshot',
1114
usage: 'minimax auth status',
1215
examples: [
1316
'minimax auth status',
@@ -18,26 +21,71 @@ export default defineCommand({
1821
const credential = await resolveCredential(config);
1922
const format = detectOutputFormat(config.output);
2023

21-
const result: Record<string, unknown> = {
22-
method: credential.method,
23-
source: credential.source,
24-
};
24+
if (format !== 'text') {
25+
const result: Record<string, unknown> = {
26+
method: credential.method,
27+
source: credential.source,
28+
};
29+
if (credential.method === 'oauth') {
30+
const creds = await loadCredentials();
31+
if (creds) {
32+
result.token_expires = creds.expires_at;
33+
if (creds.account) result.account = creds.account;
34+
}
35+
} else {
36+
result.key = credential.token.slice(0, 6) + '...' + credential.token.slice(-4);
37+
}
38+
console.log(formatOutput(result, format));
39+
return;
40+
}
41+
42+
// Text format — rich output
43+
console.log('Authentication Status:');
44+
console.log(` Method: ${credential.method}`);
45+
console.log(` Source: ${credential.source}`);
46+
47+
const token = credential.token;
48+
const maskedToken = token.length > 8 ? `${token.slice(0, 4)}...${token.slice(-4)}` : '***';
49+
console.log(` Key: ${maskedToken}`);
2550

2651
if (credential.method === 'oauth') {
2752
const creds = await loadCredentials();
2853
if (creds) {
29-
result.token_expires = creds.expires_at;
30-
if (creds.account) result.account = creds.account;
54+
if (creds.account) console.log(` Account: ${creds.account}`);
3155
const expiresAt = new Date(creds.expires_at);
3256
const minutesLeft = Math.round((expiresAt.getTime() - Date.now()) / 60000);
33-
result.expires_in = `${minutesLeft} minutes`;
57+
console.log(` Expires in: ${minutesLeft} minutes`);
3458
}
35-
result.credentials_path = '~/.minimax/credentials.json';
36-
} else {
37-
result.key = credential.token.slice(0, 6) + '...' + credential.token.slice(-4);
3859
}
3960

40-
console.log(formatOutput(result, format));
61+
// Fetch quota snapshot
62+
console.log('');
63+
process.stderr.write('Fetching quota snapshot...\n');
64+
try {
65+
const url = quotaEndpoint(config.baseUrl);
66+
const quota = await requestJson<QuotaResponse>(config, { url, method: 'GET' });
67+
const models = quota.model_remains || [];
68+
if (models.length > 0) {
69+
console.log('Available Quotas:');
70+
for (const m of models.slice(0, 5)) {
71+
const remaining = m.current_interval_total_count - m.current_interval_usage_count;
72+
const pct = m.current_interval_total_count > 0
73+
? Math.round((remaining / m.current_interval_total_count) * 100)
74+
: 0;
75+
console.log(` ${m.model_name.padEnd(24)} ${String(remaining).padStart(6)} / ${m.current_interval_total_count} (${pct}%)`);
76+
}
77+
if (models.length > 5) {
78+
console.log(` ... and ${models.length - 5} more (run 'minimax quota show' for full details)`);
79+
}
80+
} else {
81+
console.log(' No quota data available.');
82+
}
83+
} catch (e) {
84+
console.log(` Quota fetch failed: ${(e as Error).message}`);
85+
}
86+
console.log('');
87+
console.log("Run 'minimax quota show' for full details.");
88+
4189
} catch {
4290
const format = detectOutputFormat(config.output);
4391
const result = {

src/commands/image/generate.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ export default defineCommand({
9494

9595
const imageUrls = response.data.image_urls || [];
9696

97+
if (!config.quiet) {
98+
process.stderr.write('[Model: image-01]\n');
99+
}
100+
97101
// Download if --out-dir specified
98102
if (flags.outDir) {
99103
const outDir = flags.outDir as string;

src/commands/music/generate.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ export default defineCommand({
9494
body,
9595
});
9696

97+
if (!config.quiet) {
98+
process.stderr.write('[Model: music-2.5]\n');
99+
}
100+
97101
if (outPath && response.data.audio) {
98102
const audioBuffer = Buffer.from(response.data.audio, 'hex');
99103
writeFileSync(outPath, audioBuffer);

src/commands/speech/synthesize.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ export default defineCommand({
117117
body,
118118
});
119119

120+
if (!config.quiet) {
121+
process.stderr.write(`[Model: ${model}]\n`);
122+
}
123+
120124
if (outPath && response.data.audio) {
121125
// --out given: decode hex and save to file
122126
const audioBuffer = Buffer.from(response.data.audio, 'hex');

src/commands/text/chat.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ export default defineCommand({
152152
const url = chatEndpoint(config.baseUrl);
153153

154154
if (shouldStream) {
155+
if (!config.quiet) {
156+
process.stderr.write(`[Model: ${model}]\n`);
157+
}
155158
const res = await request(config, {
156159
url,
157160
method: 'POST',
@@ -211,6 +214,10 @@ export default defineCommand({
211214

212215
const text = extractText(response.content);
213216

217+
if (!config.quiet && format === 'text') {
218+
process.stderr.write(`[Model: ${response.model || model}]\n`);
219+
}
220+
214221
if (config.quiet || format === 'text') {
215222
console.log(text);
216223
} else {

src/commands/video/generate.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ export default defineCommand({
8585

8686
const taskId = response.task_id;
8787

88+
if (!config.quiet && !flags.noWait && !config.async) {
89+
process.stderr.write(`[Model: ${model}]\n`);
90+
} else if (!config.quiet) {
91+
process.stderr.write(`[Model: ${model}]\n`);
92+
}
93+
8894
// --no-wait or --async: return task ID immediately
8995
if (flags.noWait || config.async) {
9096
// Always pure JSON — Agent/CI mode needs predictable stdout

0 commit comments

Comments
 (0)