Skip to content

Commit 5bca435

Browse files
feat: Update login command to use hosted API authentication
1 parent 3920d41 commit 5bca435

4 files changed

Lines changed: 234 additions & 38 deletions

File tree

package-lock.json

Lines changed: 68 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"@google-cloud/storage": "^7.18.0",
8787
"@linear/sdk": "^68.1.0",
8888
"@modelcontextprotocol/sdk": "^0.5.0",
89+
"@stackmemoryai/stackmemory": "^0.3.19",
8990
"@types/bcryptjs": "^2.4.6",
9091
"@types/inquirer": "^9.0.9",
9192
"@types/pg": "^8.16.0",

src/cli/commands/login.ts

Lines changed: 164 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,64 +4,193 @@ import chalk from 'chalk';
44
import { homedir } from 'os';
55
import { join } from 'path';
66
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
7+
import open from 'open';
78

89
interface ConfigShape {
910
version?: string;
1011
setupCompleted?: string;
1112
features?: any;
1213
paths?: any;
1314
database?: { mode?: 'local' | 'hosted'; url?: string };
15+
auth?: {
16+
apiKey?: string;
17+
apiUrl?: string;
18+
email?: string;
19+
};
20+
}
21+
22+
interface AuthResponse {
23+
success: boolean;
24+
apiKey?: string;
25+
databaseUrl?: string;
26+
email?: string;
27+
error?: string;
1428
}
1529

1630
export function registerLoginCommand(program: Command): void {
1731
program
1832
.command('login')
19-
.description('Login to hosted StackMemory (configure managed Postgres)')
20-
.option('--open', 'Open hosted signup/login page before prompting')
33+
.description('Login to hosted StackMemory service')
34+
.option('--api-url <url>', 'Custom API URL', 'https://api.stackmemory.ai')
35+
.option('--email <email>', 'Email address for login')
36+
.option('--password <password>', 'Password (not recommended in CLI)')
2137
.action(async (options) => {
2238
const cfgDir = join(homedir(), '.stackmemory');
2339
if (!existsSync(cfgDir)) mkdirSync(cfgDir, { recursive: true });
2440

25-
if (options.open) {
26-
try {
27-
const signupUrl = 'https://stackmemory.ai/hosted';
28-
const mod = await import('open');
29-
await mod.default(signupUrl);
30-
} catch (e) {
31-
console.log(chalk.yellow('Could not open browser automatically.'));
32-
}
33-
}
41+
console.log(chalk.cyan('🔐 StackMemory Hosted Service Login\n'));
3442

35-
const { databaseUrl } = await inquirer.prompt([
43+
// Prompt for credentials
44+
const credentials = await inquirer.prompt([
45+
{
46+
type: 'input',
47+
name: 'email',
48+
message: 'Email:',
49+
default: options.email,
50+
validate: (input: string) => {
51+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
52+
return emailRegex.test(input) ? true : 'Please enter a valid email';
53+
},
54+
},
3655
{
3756
type: 'password',
38-
name: 'databaseUrl',
39-
message: 'Paste your hosted DATABASE_URL (postgres://...)',
40-
validate: (input: string) =>
41-
input.startsWith('postgres://') || input.startsWith('postgresql://')
42-
? true
43-
: 'Must start with postgres:// or postgresql://',
57+
name: 'password',
58+
message: 'Password:',
59+
default: options.password,
60+
mask: '*',
61+
validate: (input: string) => input.length >= 6 ? true : 'Password must be at least 6 characters',
4462
},
4563
]);
4664

47-
// Merge into config.json
48-
const cfgPath = join(cfgDir, 'config.json');
49-
let cfg: ConfigShape = {};
50-
try {
51-
if (existsSync(cfgPath)) cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
52-
} catch {}
53-
cfg.database = { ...(cfg.database || {}), mode: 'hosted', url: databaseUrl };
54-
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
55-
console.log(chalk.green('✓ Hosted database configured in ~/.stackmemory/config.json'));
65+
console.log(chalk.gray('\nAuthenticating with StackMemory API...'));
5666

57-
// Save env helper
5867
try {
59-
const envFile = join(cfgDir, 'railway.env');
60-
writeFileSync(envFile, `# StackMemory hosted DB\nDATABASE_URL=${databaseUrl}\n`);
61-
console.log(chalk.green('✓ Saved DATABASE_URL to ~/.stackmemory/railway.env'));
62-
} catch {}
68+
// Authenticate with the hosted API
69+
const apiUrl = options.apiUrl || process.env.STACKMEMORY_API_URL || 'https://api.stackmemory.ai';
70+
const response = await fetch(`${apiUrl}/auth/login`, {
71+
method: 'POST',
72+
headers: {
73+
'Content-Type': 'application/json',
74+
'User-Agent': 'StackMemory-CLI/0.3.19',
75+
},
76+
body: JSON.stringify({
77+
email: credentials.email,
78+
password: credentials.password,
79+
}),
80+
});
6381

64-
console.log(chalk.gray('Tip: export DATABASE_URL before starting the server.'));
65-
});
66-
}
82+
const data: AuthResponse = await response.json();
83+
84+
if (!response.ok || !data.success) {
85+
if (response.status === 404) {
86+
// Fallback to Railway server if hosted API not available
87+
console.log(chalk.yellow('\n⚠️ Hosted API not available. Would you like to:'));
88+
const { choice } = await inquirer.prompt([
89+
{
90+
type: 'list',
91+
name: 'choice',
92+
message: 'Select an option:',
93+
choices: [
94+
{ name: 'Open signup page in browser', value: 'signup' },
95+
{ name: 'Configure database URL manually', value: 'manual' },
96+
{ name: 'Use local database', value: 'local' },
97+
{ name: 'Cancel', value: 'cancel' },
98+
],
99+
},
100+
]);
101+
102+
if (choice === 'signup') {
103+
await open('https://stackmemory.ai/signup');
104+
console.log(chalk.cyan('Opening signup page in browser...'));
105+
return;
106+
} else if (choice === 'manual') {
107+
const { databaseUrl } = await inquirer.prompt([
108+
{
109+
type: 'password',
110+
name: 'databaseUrl',
111+
message: 'Enter your DATABASE_URL (postgres://...):',
112+
validate: (input: string) =>
113+
input.startsWith('postgres://') || input.startsWith('postgresql://')
114+
? true
115+
: 'Must start with postgres:// or postgresql://',
116+
},
117+
]);
67118

119+
// Save manual configuration
120+
const cfgPath = join(cfgDir, 'config.json');
121+
let cfg: ConfigShape = {};
122+
try {
123+
if (existsSync(cfgPath)) cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
124+
} catch {}
125+
126+
cfg.database = { mode: 'hosted', url: databaseUrl };
127+
cfg.auth = { email: credentials.email };
128+
129+
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
130+
console.log(chalk.green('✓ Database configured successfully'));
131+
return;
132+
} else if (choice === 'local') {
133+
const cfgPath = join(cfgDir, 'config.json');
134+
let cfg: ConfigShape = {};
135+
try {
136+
if (existsSync(cfgPath)) cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
137+
} catch {}
138+
139+
cfg.database = { mode: 'local' };
140+
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
141+
console.log(chalk.green('✓ Switched to local database mode'));
142+
return;
143+
} else {
144+
console.log(chalk.gray('Login cancelled'));
145+
return;
146+
}
147+
}
148+
149+
throw new Error(data.error || 'Authentication failed');
150+
}
151+
152+
// Save configuration
153+
const cfgPath = join(cfgDir, 'config.json');
154+
let cfg: ConfigShape = {};
155+
try {
156+
if (existsSync(cfgPath)) cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
157+
} catch {}
158+
159+
cfg.auth = {
160+
apiKey: data.apiKey,
161+
apiUrl: apiUrl,
162+
email: credentials.email,
163+
};
164+
165+
if (data.databaseUrl) {
166+
cfg.database = {
167+
mode: 'hosted',
168+
url: data.databaseUrl,
169+
};
170+
}
171+
172+
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
173+
174+
// Save environment variables
175+
const envFile = join(cfgDir, 'stackmemory.env');
176+
const envContent = `# StackMemory Authentication
177+
STACKMEMORY_API_KEY=${data.apiKey}
178+
STACKMEMORY_API_URL=${apiUrl}
179+
${data.databaseUrl ? `DATABASE_URL=${data.databaseUrl}` : ''}
180+
`;
181+
writeFileSync(envFile, envContent);
182+
183+
console.log(chalk.green('\n✅ Successfully logged in to StackMemory'));
184+
console.log(chalk.green(`✓ Configuration saved to ~/.stackmemory/config.json`));
185+
console.log(chalk.gray('\nYou can now use:'));
186+
console.log(chalk.cyan(' stackmemory sync ') + chalk.gray('- Sync your context to the cloud'));
187+
console.log(chalk.cyan(' stackmemory db status') + chalk.gray('- Check database connection'));
188+
console.log(chalk.cyan(' stackmemory context ') + chalk.gray('- Manage your contexts'));
189+
190+
} catch (error: any) {
191+
console.error(chalk.red('\n❌ Login failed:'), error.message);
192+
console.log(chalk.yellow('\nTip: Visit https://stackmemory.ai/signup to create an account'));
193+
process.exit(1);
194+
}
195+
});
196+
}

src/servers/railway/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ class RailwayMCPServer {
356356
} else if (this.db) {
357357
this.db.prepare('DELETE FROM admin_sessions WHERE datetime(expires_at) <= datetime("now")').run();
358358
}
359-
} catch (e) {
359+
} catch {
360360
console.warn('Admin session cleanup failed:', e);
361361
}
362362
};

0 commit comments

Comments
 (0)