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.
@@ -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
- 'origin': 'https://minara.ai',
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
  });
@@ -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) {
@@ -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: 'cli' });
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: 'cli',
61
- deviceType: 'cli',
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'}`);
@@ -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.startsWith('data:'))
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
- catch {
41
- // Non-JSON data line — might be raw text
42
- if (data)
43
- yield data;
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
- for await (const chunk of parseSSE(response)) {
138
- process.stdout.write(chunk);
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 sendAndPrint(userMsg);
245
+ await sendAndPrintWithPause(userMsg);
179
246
  }
180
247
  }));
@@ -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: 'cli' });
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: 'cli', deviceType: 'cli' });
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: '📧 Email verification code', value: 'email' },
143
- { name: '🔵 Google', value: 'google' },
144
- { name: '🍎 Apple ID', value: 'apple' },
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minara",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "CLI client for Minara.ai — login, trade, deposit/withdraw, chat and more from your terminal.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",