minara 0.1.3 → 0.1.5
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/api/auth.d.ts +5 -1
- package/dist/api/auth.js +11 -0
- package/dist/api/chat.js +3 -1
- package/dist/api/client.js +3 -0
- package/dist/auth-refresh.js +3 -3
- package/dist/commands/chat.js +87 -20
- package/dist/commands/login.js +73 -6
- package/dist/types.d.ts +12 -0
- package/package.json +1 -1
package/dist/api/auth.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EmailCodeDto, EmailVerifyDto, AuthUser, FavoriteTokensPayload, OAuthProvider } from '../types.js';
|
|
1
|
+
import type { EmailCodeDto, EmailVerifyDto, AuthUser, FavoriteTokensPayload, OAuthProvider, DeviceAuthStartResponse, DeviceAuthStatusResponse } from '../types.js';
|
|
2
2
|
/** Send email verification code */
|
|
3
3
|
export declare function sendEmailCode(dto: EmailCodeDto): Promise<import("../types.js").ApiResponse<void>>;
|
|
4
4
|
/** Verify email code → returns user + access_token */
|
|
@@ -28,3 +28,7 @@ export declare function addFavoriteTokens(token: string, payload: FavoriteTokens
|
|
|
28
28
|
}>>;
|
|
29
29
|
/** Get invite history */
|
|
30
30
|
export declare function getInviteHistory(token: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>[]>>;
|
|
31
|
+
/** Start device authorization flow */
|
|
32
|
+
export declare function startDeviceAuth(): Promise<import("../types.js").ApiResponse<DeviceAuthStartResponse>>;
|
|
33
|
+
/** Poll device authorization status */
|
|
34
|
+
export declare function getDeviceAuthStatus(deviceCode: string): Promise<import("../types.js").ApiResponse<DeviceAuthStatusResponse>>;
|
package/dist/api/auth.js
CHANGED
|
@@ -44,3 +44,14 @@ export function addFavoriteTokens(token, payload) {
|
|
|
44
44
|
export function getInviteHistory(token) {
|
|
45
45
|
return get('/auth/invite-history', { token });
|
|
46
46
|
}
|
|
47
|
+
// ─── Device Authorization Flow (RFC 8628) ─────────────────────────────────
|
|
48
|
+
/** Start device authorization flow */
|
|
49
|
+
export function startDeviceAuth() {
|
|
50
|
+
return post('/auth/device/start');
|
|
51
|
+
}
|
|
52
|
+
/** Poll device authorization status */
|
|
53
|
+
export function getDeviceAuthStatus(deviceCode) {
|
|
54
|
+
return post('/auth/device/status', {
|
|
55
|
+
body: { device_code: deviceCode },
|
|
56
|
+
});
|
|
57
|
+
}
|
package/dist/api/chat.js
CHANGED
|
@@ -11,7 +11,9 @@ export async function sendChatStream(token, dto) {
|
|
|
11
11
|
'Content-Type': 'application/json',
|
|
12
12
|
'Accept': 'text/event-stream',
|
|
13
13
|
'Authorization': `Bearer ${token}`,
|
|
14
|
-
'
|
|
14
|
+
'Origin': 'https://minara.ai',
|
|
15
|
+
'Referer': 'https://minara.ai/',
|
|
16
|
+
'User-Agent': 'Minara-CLI/1.0',
|
|
15
17
|
},
|
|
16
18
|
body: JSON.stringify(dto),
|
|
17
19
|
});
|
package/dist/api/client.js
CHANGED
|
@@ -22,6 +22,9 @@ async function requestImpl(path, opts, isRetry) {
|
|
|
22
22
|
const headers = {
|
|
23
23
|
'Content-Type': 'application/json',
|
|
24
24
|
'Accept': 'application/json',
|
|
25
|
+
'Origin': 'https://minara.ai',
|
|
26
|
+
'Referer': 'https://minara.ai/',
|
|
27
|
+
'User-Agent': 'Minara-CLI/1.0',
|
|
25
28
|
...extraHeaders,
|
|
26
29
|
};
|
|
27
30
|
if (token) {
|
package/dist/auth-refresh.js
CHANGED
|
@@ -42,7 +42,7 @@ export async function attemptReAuth() {
|
|
|
42
42
|
console.log(chalk.dim(`Sending verification code to ${email}…`));
|
|
43
43
|
}
|
|
44
44
|
// ── Send code ───────────────────────────────────────────────────────
|
|
45
|
-
const codeRes = await sendEmailCode({ email, platform: '
|
|
45
|
+
const codeRes = await sendEmailCode({ email, platform: 'web' });
|
|
46
46
|
if (!codeRes.success) {
|
|
47
47
|
console.error(chalk.red('✖'), `Failed to send code: ${codeRes.error?.message ?? 'Unknown error'}`);
|
|
48
48
|
return null;
|
|
@@ -57,8 +57,8 @@ export async function attemptReAuth() {
|
|
|
57
57
|
const verifyRes = await verifyEmailCode({
|
|
58
58
|
email,
|
|
59
59
|
code,
|
|
60
|
-
channel: '
|
|
61
|
-
deviceType: '
|
|
60
|
+
channel: 'web',
|
|
61
|
+
deviceType: 'desktop',
|
|
62
62
|
});
|
|
63
63
|
if (!verifyRes.success || !verifyRes.data) {
|
|
64
64
|
console.error(chalk.red('✖'), `Verification failed: ${verifyRes.error?.message ?? 'Invalid code'}`);
|
package/dist/commands/chat.js
CHANGED
|
@@ -22,25 +22,58 @@ async function* parseSSE(response) {
|
|
|
22
22
|
const lines = buffer.split('\n');
|
|
23
23
|
buffer = lines.pop() ?? '';
|
|
24
24
|
for (const line of lines) {
|
|
25
|
-
if (!line
|
|
25
|
+
if (!line)
|
|
26
|
+
continue;
|
|
27
|
+
// Handle AI SDK v5 streaming format: "type:value"
|
|
28
|
+
// e.g., "0:text", "1:reasoning", "9:tool_call"
|
|
29
|
+
const colonIndex = line.indexOf(':');
|
|
30
|
+
if (colonIndex !== -1) {
|
|
31
|
+
const type = line.slice(0, colonIndex);
|
|
32
|
+
const data = line.slice(colonIndex + 1);
|
|
33
|
+
// Type 0 is text content
|
|
34
|
+
if (type === '0' && data) {
|
|
35
|
+
try {
|
|
36
|
+
// Data might be JSON-encoded string like "text" or actual JSON
|
|
37
|
+
const parsed = JSON.parse(data);
|
|
38
|
+
if (typeof parsed === 'string') {
|
|
39
|
+
yield parsed;
|
|
40
|
+
}
|
|
41
|
+
else if (parsed.text) {
|
|
42
|
+
yield parsed.text;
|
|
43
|
+
}
|
|
44
|
+
else if (parsed.content) {
|
|
45
|
+
yield parsed.content;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// If parsing fails, treat as raw text
|
|
50
|
+
yield data;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Type 1 is reasoning (can be skipped or shown differently)
|
|
54
|
+
// Type 9 is tool_call (can be skipped for now)
|
|
26
55
|
continue;
|
|
27
|
-
const data = line.slice(5).trim();
|
|
28
|
-
if (data === '[DONE]')
|
|
29
|
-
return;
|
|
30
|
-
try {
|
|
31
|
-
const parsed = JSON.parse(data);
|
|
32
|
-
const text = parsed?.choices?.[0]?.delta?.content
|
|
33
|
-
?? parsed?.content
|
|
34
|
-
?? parsed?.text
|
|
35
|
-
?? parsed?.data?.text
|
|
36
|
-
?? (typeof parsed === 'string' ? parsed : null);
|
|
37
|
-
if (text)
|
|
38
|
-
yield text;
|
|
39
56
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
// Handle standard SSE format: "data:json"
|
|
58
|
+
if (line.startsWith('data:')) {
|
|
59
|
+
const data = line.slice(5).trim();
|
|
60
|
+
if (data === '[DONE]')
|
|
61
|
+
return;
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(data);
|
|
64
|
+
const text = parsed?.choices?.[0]?.delta?.content
|
|
65
|
+
?? parsed?.content
|
|
66
|
+
?? parsed?.text
|
|
67
|
+
?? parsed?.data?.text
|
|
68
|
+
?? (typeof parsed === 'string' ? parsed : null);
|
|
69
|
+
if (text)
|
|
70
|
+
yield text;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Non-JSON data line — might be raw text
|
|
74
|
+
if (data)
|
|
75
|
+
yield data;
|
|
76
|
+
}
|
|
44
77
|
}
|
|
45
78
|
}
|
|
46
79
|
}
|
|
@@ -134,8 +167,31 @@ export const chatCommand = new Command('chat')
|
|
|
134
167
|
error(`API error ${response.status}: ${body}`);
|
|
135
168
|
return;
|
|
136
169
|
}
|
|
137
|
-
|
|
138
|
-
|
|
170
|
+
// Debug: Check if response body exists
|
|
171
|
+
if (!response.body) {
|
|
172
|
+
console.log(chalk.dim('(No response body)'));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
let hasContent = false;
|
|
176
|
+
try {
|
|
177
|
+
for await (const chunk of parseSSE(response)) {
|
|
178
|
+
if (chunk) {
|
|
179
|
+
process.stdout.write(chunk);
|
|
180
|
+
hasContent = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
// Ignore cancellation errors
|
|
186
|
+
if (err && typeof err === 'object' && 'name' in err && err.name === 'AbortError') {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (process.env.DEBUG) {
|
|
190
|
+
console.log(chalk.dim(`\n[Stream error: ${err}]`));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!hasContent) {
|
|
194
|
+
console.log(chalk.dim('(No response content)'));
|
|
139
195
|
}
|
|
140
196
|
console.log('\n');
|
|
141
197
|
}
|
|
@@ -149,6 +205,17 @@ export const chatCommand = new Command('chat')
|
|
|
149
205
|
console.log(chalk.dim('Type your message. "exit" to quit, "/new" for new chat, "/help" for commands.'));
|
|
150
206
|
console.log('');
|
|
151
207
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
208
|
+
// Fix: Pause readline before streaming response to prevent prompt interference
|
|
209
|
+
async function sendAndPrintWithPause(msg) {
|
|
210
|
+
rl.pause(); // Pause readline to prevent prompt interference
|
|
211
|
+
try {
|
|
212
|
+
await sendAndPrint(msg);
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
rl.resume(); // Resume readline after streaming is complete
|
|
216
|
+
process.stdout.write('\n'); // Ensure clean line before next prompt
|
|
217
|
+
}
|
|
218
|
+
}
|
|
152
219
|
const prompt = () => new Promise((resolve) => {
|
|
153
220
|
rl.question(chalk.blue('You: '), resolve);
|
|
154
221
|
});
|
|
@@ -175,6 +242,6 @@ export const chatCommand = new Command('chat')
|
|
|
175
242
|
console.log(chalk.dim(' /new — New conversation\n /id — Show chat ID\n exit — Quit'));
|
|
176
243
|
continue;
|
|
177
244
|
}
|
|
178
|
-
await
|
|
245
|
+
await sendAndPrintWithPause(userMsg);
|
|
179
246
|
}
|
|
180
247
|
}));
|
package/dist/commands/login.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { input, select, confirm } from '@inquirer/prompts';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { sendEmailCode, verifyEmailCode, getOAuthUrl, getCurrentUser } from '../api/auth.js';
|
|
4
|
+
import { sendEmailCode, verifyEmailCode, getOAuthUrl, getCurrentUser, startDeviceAuth, getDeviceAuthStatus, } from '../api/auth.js';
|
|
5
5
|
import { saveCredentials, loadCredentials } from '../config.js';
|
|
6
6
|
import { success, error, info, warn, spinner, openBrowser, unwrapApi, wrapAction } from '../utils.js';
|
|
7
7
|
import { startOAuthServer } from '../oauth-server.js';
|
|
@@ -13,7 +13,7 @@ async function loginWithEmail(emailOpt) {
|
|
|
13
13
|
validate: (v) => (v.includes('@') ? true : 'Please enter a valid email'),
|
|
14
14
|
});
|
|
15
15
|
const spin = spinner('Sending verification code…');
|
|
16
|
-
const codeRes = await sendEmailCode({ email, platform: '
|
|
16
|
+
const codeRes = await sendEmailCode({ email, platform: 'web' });
|
|
17
17
|
spin.stop();
|
|
18
18
|
if (!codeRes.success) {
|
|
19
19
|
error(codeRes.error?.message ?? 'Failed to send verification code');
|
|
@@ -25,7 +25,7 @@ async function loginWithEmail(emailOpt) {
|
|
|
25
25
|
validate: (v) => (v.length > 0 ? true : 'Code is required'),
|
|
26
26
|
});
|
|
27
27
|
const spin2 = spinner('Verifying…');
|
|
28
|
-
const verifyRes = await verifyEmailCode({ email, code, channel: '
|
|
28
|
+
const verifyRes = await verifyEmailCode({ email, code, channel: 'web', deviceType: 'desktop' });
|
|
29
29
|
spin2.stop();
|
|
30
30
|
const user = unwrapApi(verifyRes, 'Verification failed');
|
|
31
31
|
const token = user.access_token;
|
|
@@ -107,12 +107,72 @@ async function loginWithOAuth(provider) {
|
|
|
107
107
|
if (result.email)
|
|
108
108
|
console.log(chalk.dim(` ${result.email}`));
|
|
109
109
|
}
|
|
110
|
+
// ─── Device login flow (RFC 8628) ─────────────────────────────────────────
|
|
111
|
+
async function loginWithDevice() {
|
|
112
|
+
info('Starting device login...');
|
|
113
|
+
const spin = spinner('Requesting device code...');
|
|
114
|
+
const startRes = await startDeviceAuth();
|
|
115
|
+
spin.stop();
|
|
116
|
+
if (!startRes.success || !startRes.data) {
|
|
117
|
+
error(startRes.error?.message ?? 'Failed to start device login');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const { device_code, user_code, verification_url, expires_in, interval } = startRes.data;
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(chalk.bold('To complete login:'));
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log(` 1. Visit: ${chalk.cyan(verification_url)}`);
|
|
125
|
+
console.log(` 2. Enter code: ${chalk.bold.yellow(user_code)}`);
|
|
126
|
+
console.log('');
|
|
127
|
+
info(`Waiting for authentication (expires in ${Math.floor(expires_in / 60)} minutes)...`);
|
|
128
|
+
info(chalk.dim('(Press Ctrl+C to cancel)'));
|
|
129
|
+
console.log('');
|
|
130
|
+
// Try to open browser
|
|
131
|
+
openBrowser(`${verification_url}?user_code=${user_code}`);
|
|
132
|
+
// Poll for completion
|
|
133
|
+
const startTime = Date.now();
|
|
134
|
+
const expiresAt = startTime + expires_in * 1000;
|
|
135
|
+
let pollInterval = interval * 1000;
|
|
136
|
+
while (Date.now() < expiresAt) {
|
|
137
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
138
|
+
const statusRes = await getDeviceAuthStatus(device_code);
|
|
139
|
+
if (!statusRes.success || !statusRes.data) {
|
|
140
|
+
// Network error, keep polling
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const data = statusRes.data;
|
|
144
|
+
const { status, access_token, user } = data;
|
|
145
|
+
if (status === 'expired') {
|
|
146
|
+
error('Device login expired. Please try again.');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
if (status === 'completed' && access_token && user) {
|
|
150
|
+
saveCredentials({
|
|
151
|
+
accessToken: access_token,
|
|
152
|
+
userId: user.id,
|
|
153
|
+
email: user.email,
|
|
154
|
+
displayName: user.displayName,
|
|
155
|
+
});
|
|
156
|
+
success('Login successful! Credentials saved to ~/.minara/');
|
|
157
|
+
if (user.displayName)
|
|
158
|
+
console.log(chalk.dim(` Welcome, ${user.displayName}`));
|
|
159
|
+
if (user.email)
|
|
160
|
+
console.log(chalk.dim(` ${user.email}`));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Still pending, show progress
|
|
164
|
+
process.stdout.write('.');
|
|
165
|
+
}
|
|
166
|
+
error('Device login timed out. Please try again.');
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
110
169
|
// ─── Command ──────────────────────────────────────────────────────────────
|
|
111
170
|
export const loginCommand = new Command('login')
|
|
112
171
|
.description('Login to your Minara account')
|
|
113
172
|
.option('-e, --email <email>', 'Login with email verification code')
|
|
114
173
|
.option('--google', 'Login with Google')
|
|
115
174
|
.option('--apple', 'Login with Apple ID')
|
|
175
|
+
.option('--device', 'Login with device code (for headless environments)')
|
|
116
176
|
.action(wrapAction(async (opts) => {
|
|
117
177
|
// Warn if already logged in
|
|
118
178
|
const existing = loadCredentials();
|
|
@@ -135,13 +195,17 @@ export const loginCommand = new Command('login')
|
|
|
135
195
|
else if (opts.apple) {
|
|
136
196
|
method = 'apple';
|
|
137
197
|
}
|
|
198
|
+
else if (opts.device) {
|
|
199
|
+
method = 'device';
|
|
200
|
+
}
|
|
138
201
|
else {
|
|
139
202
|
method = await select({
|
|
140
203
|
message: 'How would you like to login?',
|
|
141
204
|
choices: [
|
|
142
|
-
{ name: '
|
|
143
|
-
{ name: '
|
|
144
|
-
{ name: '
|
|
205
|
+
{ name: 'Email verification code', value: 'email' },
|
|
206
|
+
{ name: 'Google', value: 'google' },
|
|
207
|
+
{ name: 'Apple ID', value: 'apple' },
|
|
208
|
+
{ name: 'Device code (for headless environments)', value: 'device' },
|
|
145
209
|
],
|
|
146
210
|
});
|
|
147
211
|
}
|
|
@@ -149,6 +213,9 @@ export const loginCommand = new Command('login')
|
|
|
149
213
|
if (method === 'email') {
|
|
150
214
|
await loginWithEmail(opts.email);
|
|
151
215
|
}
|
|
216
|
+
else if (method === 'device') {
|
|
217
|
+
await loginWithDevice();
|
|
218
|
+
}
|
|
152
219
|
else {
|
|
153
220
|
await loginWithOAuth(method);
|
|
154
221
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -33,6 +33,18 @@ export interface AuthUser {
|
|
|
33
33
|
invitationCode?: string;
|
|
34
34
|
mfaSettings?: Record<string, unknown>;
|
|
35
35
|
}
|
|
36
|
+
export interface DeviceAuthStartResponse {
|
|
37
|
+
device_code: string;
|
|
38
|
+
user_code: string;
|
|
39
|
+
verification_url: string;
|
|
40
|
+
expires_in: number;
|
|
41
|
+
interval: number;
|
|
42
|
+
}
|
|
43
|
+
export interface DeviceAuthStatusResponse {
|
|
44
|
+
status: 'pending' | 'completed' | 'expired';
|
|
45
|
+
access_token?: string;
|
|
46
|
+
user?: AuthUser;
|
|
47
|
+
}
|
|
36
48
|
export interface FavoriteTokensPayload {
|
|
37
49
|
tokens: string[];
|
|
38
50
|
}
|