mistagent 0.1.20 → 0.1.22
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/src/api/auth.d.ts +15 -2
- package/dist/src/api/auth.js +16 -6
- package/dist/src/api/client.js +3 -8
- package/dist/src/api/quota.d.ts +13 -0
- package/dist/src/api/quota.js +7 -0
- package/dist/src/components/LoginPrompt.d.ts +1 -1
- package/dist/src/components/LoginPrompt.js +10 -10
- package/dist/src/hooks/useChat.js +23 -2
- package/dist/src/hooks/useSlashCommand.js +45 -0
- package/dist/src/main.js +13 -4
- package/dist/src/types/api.d.ts +0 -11
- package/dist/src/utils/config.d.ts +1 -1
- package/dist/src/utils/config.js +2 -2
- package/dist/src/utils/constants.d.ts +1 -1
- package/dist/src/utils/constants.js +1 -1
- package/package.json +1 -1
package/dist/src/api/auth.d.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import type { AuthStatus,
|
|
1
|
+
import type { AuthStatus, UserInfo } from '../types/api.js';
|
|
2
2
|
export declare const authApi: {
|
|
3
3
|
getStatus(): Promise<AuthStatus>;
|
|
4
|
-
login(username: string, password: string): Promise<LoginResponse>;
|
|
5
4
|
getMe(): Promise<UserInfo>;
|
|
6
5
|
};
|
|
6
|
+
/** Supabase 直连登录 — POST /auth/v1/token?grant_type=password */
|
|
7
|
+
export interface SupabaseLoginResult {
|
|
8
|
+
access_token: string;
|
|
9
|
+
refresh_token: string;
|
|
10
|
+
user: {
|
|
11
|
+
id: string;
|
|
12
|
+
email: string;
|
|
13
|
+
user_metadata: {
|
|
14
|
+
username?: string;
|
|
15
|
+
display_name?: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export declare function supabaseLogin(supabaseUrl: string, anonKey: string, email: string, password: string): Promise<SupabaseLoginResult>;
|
|
7
20
|
//# sourceMappingURL=auth.d.ts.map
|
package/dist/src/api/auth.js
CHANGED
|
@@ -3,14 +3,24 @@ export const authApi = {
|
|
|
3
3
|
getStatus() {
|
|
4
4
|
return getClient().get('/api/v1/auth/status');
|
|
5
5
|
},
|
|
6
|
-
login(username, password) {
|
|
7
|
-
return getClient().post('/api/v1/auth/login', {
|
|
8
|
-
username,
|
|
9
|
-
password,
|
|
10
|
-
});
|
|
11
|
-
},
|
|
12
6
|
getMe() {
|
|
13
7
|
return getClient().get('/api/v1/auth/me');
|
|
14
8
|
},
|
|
15
9
|
};
|
|
10
|
+
export async function supabaseLogin(supabaseUrl, anonKey, email, password) {
|
|
11
|
+
const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=password`, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: {
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
'apikey': anonKey,
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify({ email, password }),
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
const body = await res.json().catch(() => null);
|
|
21
|
+
const msg = body?.error_description || body?.msg || 'Login failed';
|
|
22
|
+
throw new Error(msg);
|
|
23
|
+
}
|
|
24
|
+
return res.json();
|
|
25
|
+
}
|
|
16
26
|
//# sourceMappingURL=auth.js.map
|
package/dist/src/api/client.js
CHANGED
|
@@ -89,11 +89,9 @@ export class ApiClient {
|
|
|
89
89
|
for (const [k, v] of Object.entries(params)) {
|
|
90
90
|
url.searchParams.set(k, v);
|
|
91
91
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
const res = await fetch(url.toString());
|
|
92
|
+
const res = await fetch(url.toString(), {
|
|
93
|
+
headers: this.authHeaders(),
|
|
94
|
+
});
|
|
97
95
|
if (!res.ok) {
|
|
98
96
|
throw new ApiError(res.status, await res.text());
|
|
99
97
|
}
|
|
@@ -105,9 +103,6 @@ export class ApiClient {
|
|
|
105
103
|
// SSE streaming via POST - for large payloads (e.g. messages with file contents)
|
|
106
104
|
async streamPost(path, body) {
|
|
107
105
|
const url = new URL(path, this.baseUrl);
|
|
108
|
-
if (this.token) {
|
|
109
|
-
url.searchParams.set('token', this.token);
|
|
110
|
-
}
|
|
111
106
|
const res = await fetch(url.toString(), {
|
|
112
107
|
method: 'POST',
|
|
113
108
|
headers: {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface QuotaBalance {
|
|
2
|
+
usd_balance: number;
|
|
3
|
+
usd_total_granted: number;
|
|
4
|
+
usd_total_consumed: number;
|
|
5
|
+
usage_today_tokens: number;
|
|
6
|
+
usage_this_month_tokens: number;
|
|
7
|
+
cost_today_usd: number;
|
|
8
|
+
cost_this_month_usd: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const quotaApi: {
|
|
11
|
+
balance(): Promise<QuotaBalance>;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=quota.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
interface LoginPromptProps {
|
|
3
|
-
onLogin: (
|
|
3
|
+
onLogin: (email: string, password: string) => Promise<void>;
|
|
4
4
|
error?: string | null;
|
|
5
5
|
}
|
|
6
6
|
export declare const LoginPrompt: React.FC<LoginPromptProps>;
|
|
@@ -46,8 +46,8 @@ const SimpleInput = ({ value, onChange, onSubmit, mask }) => {
|
|
|
46
46
|
// ── LoginPrompt ──────────────────────────────────────────
|
|
47
47
|
export const LoginPrompt = ({ onLogin, error, }) => {
|
|
48
48
|
const { subscribe, unsubscribe } = useKeypressContext();
|
|
49
|
-
const [step, setStep] = useState('
|
|
50
|
-
const [
|
|
49
|
+
const [step, setStep] = useState('email');
|
|
50
|
+
const [email, setEmail] = useState('');
|
|
51
51
|
const [password, setPassword] = useState('');
|
|
52
52
|
const [isLoading, setIsLoading] = useState(false);
|
|
53
53
|
const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false);
|
|
@@ -72,9 +72,9 @@ export const LoginPrompt = ({ onLogin, error, }) => {
|
|
|
72
72
|
subscribe(ctrlCHandler, KeypressPriority.Normal);
|
|
73
73
|
return () => unsubscribe(ctrlCHandler);
|
|
74
74
|
}, [ctrlCHandler, subscribe, unsubscribe]);
|
|
75
|
-
const
|
|
75
|
+
const handleEmailSubmit = useCallback((val) => {
|
|
76
76
|
if (val.trim()) {
|
|
77
|
-
|
|
77
|
+
setEmail(val.trim());
|
|
78
78
|
setStep('password');
|
|
79
79
|
}
|
|
80
80
|
}, []);
|
|
@@ -83,19 +83,19 @@ export const LoginPrompt = ({ onLogin, error, }) => {
|
|
|
83
83
|
setPassword(val.trim());
|
|
84
84
|
setIsLoading(true);
|
|
85
85
|
try {
|
|
86
|
-
await onLogin(
|
|
86
|
+
await onLogin(email, val.trim());
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
finally {
|
|
89
89
|
setIsLoading(false);
|
|
90
|
-
setStep('
|
|
91
|
-
|
|
90
|
+
setStep('email');
|
|
91
|
+
setEmail('');
|
|
92
92
|
setPassword('');
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
}, [onLogin,
|
|
95
|
+
}, [onLogin, email]);
|
|
96
96
|
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", padding: 1, children: [_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [BANNER_LINES.map((line, i) => {
|
|
97
97
|
const gradient = getBannerGradient();
|
|
98
98
|
return (_jsx(Text, { color: gradient[i] ?? gradient[0], children: line }, i));
|
|
99
|
-
}), _jsx(Text, { color: palette.textMuted, children: BANNER_SUBTITLE }), _jsx(Text, { color: palette.textDim, children: ' v' + VERSION })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: error ? palette.error : palette.border, paddingX: 3, paddingY: 1, width: 48, children: [error && (_jsx(Box, { marginBottom: 1, justifyContent: "center", children: _jsxs(Text, { color: palette.error, children: ['✗ ', error] }) })), isLoading ? (_jsx(Box, { justifyContent: "center", paddingY: 1, children: _jsx(Spinner, { label: "Authenticating..." }) })) : step === '
|
|
99
|
+
}), _jsx(Text, { color: palette.textMuted, children: BANNER_SUBTITLE }), _jsx(Text, { color: palette.textDim, children: ' v' + VERSION })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: error ? palette.error : palette.border, paddingX: 3, paddingY: 1, width: 48, children: [error && (_jsx(Box, { marginBottom: 1, justifyContent: "center", children: _jsxs(Text, { color: palette.error, children: ['✗ ', error] }) })), isLoading ? (_jsx(Box, { justifyContent: "center", paddingY: 1, children: _jsx(Spinner, { label: "Authenticating..." }) })) : step === 'email' ? (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: palette.primary, bold: true, children: ' ❯ Email ' }), _jsx(SimpleInput, { value: email, onChange: setEmail, onSubmit: handleEmailSubmit })] }), _jsx(Box, { children: _jsx(Text, { color: palette.textDim, children: ' ○ Password' }) })] })) : (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: palette.success, children: ' ✓ ' }), _jsx(Text, { color: palette.textMuted, children: 'Email ' }), _jsx(Text, { children: email })] }), _jsxs(Box, { children: [_jsx(Text, { color: palette.primary, bold: true, children: ' ❯ Password ' }), _jsx(SimpleInput, { value: password, onChange: setPassword, onSubmit: handlePasswordSubmit, mask: "\u2022" })] })] })), _jsx(Box, { justifyContent: "center", marginTop: 1, children: ctrlCPressedOnce ? (_jsx(Text, { color: palette.warning, bold: true, children: "Press Ctrl-C again to exit" })) : (_jsxs(Text, { color: palette.textDim, dimColor: true, children: ["Press ", _jsx(Text, { color: palette.textMuted, children: "Enter" }), " to continue"] })) })] })] }));
|
|
100
100
|
};
|
|
101
101
|
//# sourceMappingURL=LoginPrompt.js.map
|
|
@@ -3,6 +3,7 @@ import { createParser } from 'eventsource-parser';
|
|
|
3
3
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { chatApi } from '../api/chat.js';
|
|
6
|
+
import { ApiError } from '../api/client.js';
|
|
6
7
|
import { submitTunnelResult } from '../api/tunnel.js';
|
|
7
8
|
import { processFileReferences } from '../utils/fileRef.js';
|
|
8
9
|
import { executeFileOperation, isSensitiveOperation, isAlwaysConfirmRequired } from '../utils/fileTunnel.js';
|
|
@@ -292,7 +293,17 @@ export function useChat() {
|
|
|
292
293
|
}
|
|
293
294
|
catch (err) {
|
|
294
295
|
tokenBuf.dispose();
|
|
295
|
-
|
|
296
|
+
let errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
297
|
+
if (err instanceof ApiError) {
|
|
298
|
+
try {
|
|
299
|
+
const parsed = JSON.parse(err.body);
|
|
300
|
+
if (parsed.detail?.message)
|
|
301
|
+
errorMessage = parsed.detail.message;
|
|
302
|
+
else if (typeof parsed.detail === 'string')
|
|
303
|
+
errorMessage = parsed.detail;
|
|
304
|
+
}
|
|
305
|
+
catch { /* use default */ }
|
|
306
|
+
}
|
|
296
307
|
dispatch({ type: 'STREAM_ERROR', error: errorMessage });
|
|
297
308
|
}
|
|
298
309
|
finally {
|
|
@@ -421,7 +432,17 @@ export function useChat() {
|
|
|
421
432
|
await processConfirmStream(reader);
|
|
422
433
|
}
|
|
423
434
|
catch (err) {
|
|
424
|
-
|
|
435
|
+
let errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
436
|
+
if (err instanceof ApiError) {
|
|
437
|
+
try {
|
|
438
|
+
const parsed = JSON.parse(err.body);
|
|
439
|
+
if (parsed.detail?.message)
|
|
440
|
+
errorMessage = parsed.detail.message;
|
|
441
|
+
else if (typeof parsed.detail === 'string')
|
|
442
|
+
errorMessage = parsed.detail;
|
|
443
|
+
}
|
|
444
|
+
catch { /* use default */ }
|
|
445
|
+
}
|
|
425
446
|
dispatch({ type: 'STREAM_ERROR', error: errorMessage });
|
|
426
447
|
}
|
|
427
448
|
}, [state.threadId, state.pendingPlan, dispatch, processConfirmStream]);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { sessionsApi } from '../api/sessions.js';
|
|
3
|
+
import { quotaApi } from '../api/quota.js';
|
|
3
4
|
import { useChatState, useChatDispatch } from '../contexts/ChatContext.js';
|
|
4
5
|
import { useAppState, useAppActions } from '../contexts/AppContext.js';
|
|
5
6
|
import { useUIActions } from '../contexts/UIContext.js';
|
|
@@ -104,6 +105,50 @@ export function useSlashCommand() {
|
|
|
104
105
|
});
|
|
105
106
|
return;
|
|
106
107
|
}
|
|
108
|
+
// /usage — show account balance and usage
|
|
109
|
+
if (trimmed === '/usage') {
|
|
110
|
+
dispatch({ type: 'SET_BUSY', busy: true });
|
|
111
|
+
try {
|
|
112
|
+
const b = await quotaApi.balance();
|
|
113
|
+
const fmt = (v) => `$${v.toFixed(4)}`;
|
|
114
|
+
const fmtTokens = (v) => v >= 1_000_000
|
|
115
|
+
? `${(v / 1_000_000).toFixed(1)}M`
|
|
116
|
+
: v >= 1_000
|
|
117
|
+
? `${(v / 1_000).toFixed(1)}K`
|
|
118
|
+
: String(v);
|
|
119
|
+
const lines = [
|
|
120
|
+
`**账户余额**`,
|
|
121
|
+
` 余额: ${fmt(b.usd_balance)}`,
|
|
122
|
+
` 总充值: ${fmt(b.usd_total_granted)}`,
|
|
123
|
+
` 总消耗: ${fmt(b.usd_total_consumed)}`,
|
|
124
|
+
``,
|
|
125
|
+
`**今日用量**`,
|
|
126
|
+
` Tokens: ${fmtTokens(b.usage_today_tokens)}`,
|
|
127
|
+
` 费用: ${fmt(b.cost_today_usd)}`,
|
|
128
|
+
``,
|
|
129
|
+
`**本月用量**`,
|
|
130
|
+
` Tokens: ${fmtTokens(b.usage_this_month_tokens)}`,
|
|
131
|
+
` 费用: ${fmt(b.cost_this_month_usd)}`,
|
|
132
|
+
];
|
|
133
|
+
dispatch({
|
|
134
|
+
type: 'ADD_ITEM',
|
|
135
|
+
item: { type: 'command', command: '/usage', result: lines.join('\n') },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
dispatch({
|
|
140
|
+
type: 'ADD_ITEM',
|
|
141
|
+
item: {
|
|
142
|
+
type: 'error',
|
|
143
|
+
text: `查询用量失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
dispatch({ type: 'SET_BUSY', busy: false });
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
107
152
|
// /quit or /exit
|
|
108
153
|
if (trimmed === '/quit' || trimmed === '/exit') {
|
|
109
154
|
dispatch({
|
package/dist/src/main.js
CHANGED
|
@@ -3,7 +3,7 @@ import { render } from 'ink';
|
|
|
3
3
|
import yargs from 'yargs';
|
|
4
4
|
import { hideBin } from 'yargs/helpers';
|
|
5
5
|
import { initClient } from './api/client.js';
|
|
6
|
-
import { authApi } from './api/auth.js';
|
|
6
|
+
import { authApi, supabaseLogin } from './api/auth.js';
|
|
7
7
|
import { modelsApi } from './api/models.js';
|
|
8
8
|
import { toolsApi } from './api/tools.js';
|
|
9
9
|
import { skillsApi } from './api/skills.js';
|
|
@@ -197,6 +197,7 @@ export async function main() {
|
|
|
197
197
|
{ name: '/tools', description: '显示可用工具列表', usage: '/tools' },
|
|
198
198
|
{ name: '/theme', description: '切换显示主题', usage: '/theme' },
|
|
199
199
|
{ name: '/init', description: '分析当前目录代码并生成知识图谱', usage: '/init' },
|
|
200
|
+
{ name: '/usage', description: '查看账户使用量和余额', usage: '/usage' },
|
|
200
201
|
{ name: '/quit', description: '退出应用', usage: '/quit', aliases: ['/exit'] },
|
|
201
202
|
{ name: '/logout', description: '退出登录并返回登录界面', usage: '/logout' },
|
|
202
203
|
];
|
|
@@ -210,16 +211,24 @@ export async function main() {
|
|
|
210
211
|
terminalHeight: process.stdout.rows || 24,
|
|
211
212
|
});
|
|
212
213
|
const renderApp = (overrides = {}) => (_jsx(App, { serverUrl: serverUrl, token: overrides.token ?? token, username: overrides.username ?? username, authEnabled: authStatus.auth_enabled, version: healthData?.version ?? VERSION, healthData: healthData, availableCommands: commands, availableTools: tools, initialModels: models, initialCurrentModel: currentModel, initialSessions: initialSessions, initialTheme: initialTheme, onLogin: handleLogin, onLogout: handleLogout, loginError: overrides.loginError ?? loginError, isAuthenticated: overrides.isAuthenticated ?? isAuthenticated, ...getTerminalSize() }));
|
|
213
|
-
const handleLogin = async (
|
|
214
|
+
const handleLogin = async (email, pass) => {
|
|
215
|
+
const sbUrl = authStatus.supabase_url;
|
|
216
|
+
const sbKey = authStatus.supabase_anon_key;
|
|
217
|
+
if (!sbUrl || !sbKey) {
|
|
218
|
+
loginError = 'Supabase not configured on server';
|
|
219
|
+
rerender(renderApp({ token: null, username: null, isAuthenticated: false, loginError }));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
214
222
|
try {
|
|
215
|
-
const res = await
|
|
223
|
+
const res = await supabaseLogin(sbUrl, sbKey, email, pass);
|
|
216
224
|
client.setToken(res.access_token);
|
|
217
225
|
saveToken(res.access_token);
|
|
218
226
|
if (res.refresh_token) {
|
|
219
227
|
saveRefreshToken(res.refresh_token);
|
|
220
228
|
}
|
|
221
229
|
token = res.access_token;
|
|
222
|
-
|
|
230
|
+
const meta = res.user?.user_metadata ?? {};
|
|
231
|
+
username = meta.display_name || meta.username || email;
|
|
223
232
|
isAuthenticated = true;
|
|
224
233
|
loginError = null;
|
|
225
234
|
startTokenRefresh();
|
package/dist/src/types/api.d.ts
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
export interface LoginResponse {
|
|
2
|
-
success: boolean;
|
|
3
|
-
access_token: string;
|
|
4
|
-
refresh_token?: string;
|
|
5
|
-
token_type: string;
|
|
6
|
-
user: {
|
|
7
|
-
user_id: string;
|
|
8
|
-
username: string;
|
|
9
|
-
display_name: string;
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
1
|
export interface UserInfo {
|
|
13
2
|
success: boolean;
|
|
14
3
|
user_id: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const DEFAULT_SERVER_URL = "https://
|
|
1
|
+
export declare const DEFAULT_SERVER_URL = "https://agent.slowmist.ai";
|
|
2
2
|
export declare function getServerUrl(cliArg?: string): string;
|
|
3
3
|
export declare function saveToken(token: string): void;
|
|
4
4
|
export declare function loadToken(): string | null;
|
package/dist/src/utils/config.js
CHANGED
|
@@ -9,8 +9,8 @@ const TOKEN_FILE = join(CONFIG_DIR, 'token');
|
|
|
9
9
|
// Supabase refresh token(用于自动续期 access_token)
|
|
10
10
|
const REFRESH_TOKEN_FILE = join(CONFIG_DIR, 'refresh_token');
|
|
11
11
|
// 后端服务器默认地址,可通过 CLI 参数或环境变量 MISTAGENT_SERVER 覆盖
|
|
12
|
-
// 发布时由 publish.sh 自动切换为 'https://
|
|
13
|
-
export const DEFAULT_SERVER_URL = 'https://
|
|
12
|
+
// 发布时由 publish.sh 自动切换为 'https://agent.slowmist.ai',发布后自动改回
|
|
13
|
+
export const DEFAULT_SERVER_URL = 'https://agent.slowmist.ai';
|
|
14
14
|
// 获取后端服务器地址,优先级: CLI 参数 > 环境变量 > 默认值
|
|
15
15
|
// 非 localhost 地址强制要求 HTTPS,防止中间人攻击导致 token 泄露
|
|
16
16
|
export function getServerUrl(cliArg) {
|