@kylewadegrove/cutline-mcp-cli 0.1.0 ā 0.2.0
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/commands/login.d.ts +1 -0
- package/dist/commands/login.js +37 -7
- package/dist/commands/status.js +2 -3
- package/dist/commands/upgrade.d.ts +3 -0
- package/dist/commands/upgrade.js +99 -0
- package/dist/index.js +7 -0
- package/dist/utils/config-store.d.ts +8 -0
- package/dist/utils/config-store.js +40 -0
- package/dist/utils/config.js +6 -2
- package/package.json +2 -2
- package/src/commands/login.ts +38 -8
- package/src/commands/status.ts +2 -3
- package/src/commands/upgrade.ts +118 -0
- package/src/index.ts +8 -0
- package/src/utils/config-store.ts +43 -0
- package/src/utils/config.ts +6 -2
package/dist/commands/login.d.ts
CHANGED
package/dist/commands/login.js
CHANGED
|
@@ -9,6 +9,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
9
9
|
const ora_1 = __importDefault(require("ora"));
|
|
10
10
|
const callback_js_1 = require("../auth/callback.js");
|
|
11
11
|
const keychain_js_1 = require("../auth/keychain.js");
|
|
12
|
+
const config_store_js_1 = require("../utils/config-store.js");
|
|
12
13
|
const config_js_1 = require("../utils/config.js");
|
|
13
14
|
async function exchangeCustomToken(customToken, apiKey) {
|
|
14
15
|
const response = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`, {
|
|
@@ -31,15 +32,20 @@ async function exchangeCustomToken(customToken, apiKey) {
|
|
|
31
32
|
}
|
|
32
33
|
async function loginCommand(options) {
|
|
33
34
|
const config = (0, config_js_1.getConfig)(options);
|
|
34
|
-
|
|
35
|
+
if (options.signup) {
|
|
36
|
+
console.log(chalk_1.default.bold('\nš Cutline MCP - Create Account\n'));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.log(chalk_1.default.bold('\nš Cutline MCP Authentication\n'));
|
|
40
|
+
}
|
|
35
41
|
if (options.staging) {
|
|
36
42
|
console.log(chalk_1.default.yellow(' ā ļø Using STAGING environment\n'));
|
|
37
43
|
}
|
|
38
44
|
if (!config.FIREBASE_API_KEY) {
|
|
39
|
-
|
|
40
|
-
console.error(chalk_1.default.red(`Error: ${varName} environment variable is required.`));
|
|
45
|
+
console.error(chalk_1.default.red(`Error: FIREBASE_API_KEY or NEXT_PUBLIC_FIREBASE_API_KEY environment variable is required.`));
|
|
41
46
|
console.error(chalk_1.default.gray('Please set it before running this command:'));
|
|
42
|
-
console.error(chalk_1.default.cyan(` export
|
|
47
|
+
console.error(chalk_1.default.cyan(` export FIREBASE_API_KEY=AIzaSy...`));
|
|
48
|
+
console.error(chalk_1.default.gray(' (or use NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config)'));
|
|
43
49
|
process.exit(1);
|
|
44
50
|
}
|
|
45
51
|
const spinner = (0, ora_1.default)('Starting authentication flow...').start();
|
|
@@ -48,17 +54,41 @@ async function loginCommand(options) {
|
|
|
48
54
|
spinner.text = 'Waiting for authentication...';
|
|
49
55
|
const serverPromise = (0, callback_js_1.startCallbackServer)();
|
|
50
56
|
// Open browser
|
|
51
|
-
|
|
57
|
+
let authUrl = `${config.AUTH_URL}?callback=${encodeURIComponent(config.CALLBACK_URL)}`;
|
|
58
|
+
if (options.signup) {
|
|
59
|
+
authUrl += '&mode=signup';
|
|
60
|
+
}
|
|
52
61
|
await (0, open_1.default)(authUrl);
|
|
53
|
-
spinner.text =
|
|
62
|
+
spinner.text = options.signup
|
|
63
|
+
? 'Browser opened - please create your account'
|
|
64
|
+
: 'Browser opened - please sign in or create an account';
|
|
54
65
|
// Wait for callback with custom token
|
|
55
66
|
const result = await serverPromise;
|
|
56
67
|
// Exchange custom token for refresh token
|
|
57
68
|
spinner.text = 'Exchanging token...';
|
|
58
69
|
const { refreshToken, email } = await exchangeCustomToken(result.token, config.FIREBASE_API_KEY);
|
|
59
70
|
// Store refresh token
|
|
60
|
-
|
|
71
|
+
try {
|
|
72
|
+
await (0, keychain_js_1.storeRefreshToken)(refreshToken);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.warn(chalk_1.default.yellow(' ā ļø Could not save to Keychain (skipping)'));
|
|
76
|
+
}
|
|
77
|
+
// Always save to file config (cross-platform)
|
|
78
|
+
try {
|
|
79
|
+
(0, config_store_js_1.saveConfig)({
|
|
80
|
+
refreshToken,
|
|
81
|
+
environment: options.staging ? 'staging' : 'production',
|
|
82
|
+
firebaseApiKey: config.FIREBASE_API_KEY
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error(chalk_1.default.red(' ā Failed to save config file:'), error);
|
|
87
|
+
}
|
|
61
88
|
spinner.succeed(chalk_1.default.green('Successfully authenticated!'));
|
|
89
|
+
// Show environment indicator
|
|
90
|
+
const envLabel = options.staging ? chalk_1.default.yellow('STAGING') : chalk_1.default.green('PRODUCTION');
|
|
91
|
+
console.log(chalk_1.default.gray(` Environment: ${envLabel}`));
|
|
62
92
|
if (email || result.email) {
|
|
63
93
|
console.log(chalk_1.default.gray(` Logged in as: ${email || result.email}`));
|
|
64
94
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -47,9 +47,8 @@ async function statusCommand(options) {
|
|
|
47
47
|
// Get config for API key
|
|
48
48
|
const config = (0, config_js_1.getConfig)(options);
|
|
49
49
|
if (!config.FIREBASE_API_KEY) {
|
|
50
|
-
const varName = options.staging ? 'FIREBASE_API_KEY_STAGING' : 'FIREBASE_API_KEY';
|
|
51
50
|
spinner.fail(chalk_1.default.red('Configuration error'));
|
|
52
|
-
console.error(chalk_1.default.red(`
|
|
51
|
+
console.error(chalk_1.default.red(` FIREBASE_API_KEY or NEXT_PUBLIC_FIREBASE_API_KEY environment variable is required.`));
|
|
53
52
|
process.exit(1);
|
|
54
53
|
}
|
|
55
54
|
// Exchange refresh token for ID token
|
|
@@ -57,7 +56,7 @@ async function statusCommand(options) {
|
|
|
57
56
|
const idToken = await exchangeRefreshToken(refreshToken, config.FIREBASE_API_KEY);
|
|
58
57
|
// Initialize Firebase Admin with correct project ID
|
|
59
58
|
if (firebase_admin_1.default.apps.length === 0) {
|
|
60
|
-
const projectId = options.staging ? '
|
|
59
|
+
const projectId = options.staging ? 'cutline-staging' : 'cutline-prod';
|
|
61
60
|
firebase_admin_1.default.initializeApp({ projectId });
|
|
62
61
|
}
|
|
63
62
|
// Verify the ID token
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.upgradeCommand = upgradeCommand;
|
|
7
|
+
const open_1 = __importDefault(require("open"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const ora_1 = __importDefault(require("ora"));
|
|
10
|
+
const callback_js_1 = require("../auth/callback.js");
|
|
11
|
+
const keychain_js_1 = require("../auth/keychain.js");
|
|
12
|
+
const config_store_js_1 = require("../utils/config-store.js");
|
|
13
|
+
const config_js_1 = require("../utils/config.js");
|
|
14
|
+
async function exchangeCustomToken(customToken, apiKey) {
|
|
15
|
+
const response = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
token: customToken,
|
|
20
|
+
returnSecureToken: true,
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
const error = await response.text();
|
|
25
|
+
throw new Error(`Failed to exchange custom token: ${error}`);
|
|
26
|
+
}
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
return {
|
|
29
|
+
refreshToken: data.refreshToken,
|
|
30
|
+
email: data.email,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function upgradeCommand(options) {
|
|
34
|
+
const config = (0, config_js_1.getConfig)(options);
|
|
35
|
+
console.log(chalk_1.default.bold('\nā¬ļø Cutline MCP - Upgrade to Premium\n'));
|
|
36
|
+
if (options.staging) {
|
|
37
|
+
console.log(chalk_1.default.yellow(' ā ļø Using STAGING environment\n'));
|
|
38
|
+
}
|
|
39
|
+
if (!config.FIREBASE_API_KEY) {
|
|
40
|
+
console.error(chalk_1.default.red(`Error: FIREBASE_API_KEY environment variable is required.`));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
// Determine upgrade URL based on environment
|
|
44
|
+
const baseUrl = options.staging
|
|
45
|
+
? 'https://cutline-staging.web.app'
|
|
46
|
+
: 'https://thecutline.ai';
|
|
47
|
+
console.log(chalk_1.default.gray(' Opening upgrade page in your browser...\n'));
|
|
48
|
+
console.log(chalk_1.default.dim(' After upgrading, your MCP session will be refreshed automatically.\n'));
|
|
49
|
+
const spinner = (0, ora_1.default)('Waiting for upgrade and re-authentication...').start();
|
|
50
|
+
try {
|
|
51
|
+
// Start callback server for re-auth after upgrade
|
|
52
|
+
const serverPromise = (0, callback_js_1.startCallbackServer)();
|
|
53
|
+
// Open upgrade page with callback for re-auth
|
|
54
|
+
// The upgrade page will redirect to mcp-auth after successful upgrade
|
|
55
|
+
const upgradeUrl = `${baseUrl}/upgrade?mcp_callback=${encodeURIComponent(config.CALLBACK_URL)}`;
|
|
56
|
+
await (0, open_1.default)(upgradeUrl);
|
|
57
|
+
spinner.text = 'Browser opened - complete your upgrade, then re-authenticate';
|
|
58
|
+
// Wait for callback with new token (after upgrade + re-auth)
|
|
59
|
+
const result = await serverPromise;
|
|
60
|
+
// Exchange custom token for refresh token
|
|
61
|
+
spinner.text = 'Refreshing your session...';
|
|
62
|
+
const { refreshToken, email } = await exchangeCustomToken(result.token, config.FIREBASE_API_KEY);
|
|
63
|
+
// Store refresh token
|
|
64
|
+
try {
|
|
65
|
+
await (0, keychain_js_1.storeRefreshToken)(refreshToken);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.warn(chalk_1.default.yellow(' ā ļø Could not save to Keychain (skipping)'));
|
|
69
|
+
}
|
|
70
|
+
// Save to file config
|
|
71
|
+
try {
|
|
72
|
+
(0, config_store_js_1.saveConfig)({
|
|
73
|
+
refreshToken,
|
|
74
|
+
environment: options.staging ? 'staging' : 'production',
|
|
75
|
+
firebaseApiKey: config.FIREBASE_API_KEY
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error(chalk_1.default.red(' ā Failed to save config file:'), error);
|
|
80
|
+
}
|
|
81
|
+
spinner.succeed(chalk_1.default.green('Upgrade complete! Session refreshed.'));
|
|
82
|
+
const envLabel = options.staging ? chalk_1.default.yellow('STAGING') : chalk_1.default.green('PRODUCTION');
|
|
83
|
+
console.log(chalk_1.default.gray(` Environment: ${envLabel}`));
|
|
84
|
+
if (email || result.email) {
|
|
85
|
+
console.log(chalk_1.default.gray(` Account: ${email || result.email}`));
|
|
86
|
+
}
|
|
87
|
+
console.log(chalk_1.default.green('\n ⨠Premium features are now available!\n'));
|
|
88
|
+
console.log(chalk_1.default.dim(' Try:'), chalk_1.default.cyan('exploration_graduate'), chalk_1.default.dim('to run a full Deep Dive\n'));
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
spinner.fail(chalk_1.default.red('Upgrade flow failed'));
|
|
92
|
+
if (error instanceof Error) {
|
|
93
|
+
console.error(chalk_1.default.red(` ${error.message}`));
|
|
94
|
+
}
|
|
95
|
+
console.log(chalk_1.default.gray('\n You can also upgrade at:'), chalk_1.default.cyan(`${baseUrl}/upgrade`));
|
|
96
|
+
console.log(chalk_1.default.gray(' Then run:'), chalk_1.default.cyan('cutline-mcp login'), chalk_1.default.gray('to refresh your session\n'));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const commander_1 = require("commander");
|
|
|
5
5
|
const login_js_1 = require("./commands/login.js");
|
|
6
6
|
const logout_js_1 = require("./commands/logout.js");
|
|
7
7
|
const status_js_1 = require("./commands/status.js");
|
|
8
|
+
const upgrade_js_1 = require("./commands/upgrade.js");
|
|
8
9
|
const program = new commander_1.Command();
|
|
9
10
|
program
|
|
10
11
|
.name('cutline-mcp')
|
|
@@ -14,6 +15,7 @@ program
|
|
|
14
15
|
.command('login')
|
|
15
16
|
.description('Authenticate with Cutline and store credentials')
|
|
16
17
|
.option('--staging', 'Use staging environment')
|
|
18
|
+
.option('--signup', 'Open sign-up page instead of sign-in')
|
|
17
19
|
.action(login_js_1.loginCommand);
|
|
18
20
|
program
|
|
19
21
|
.command('logout')
|
|
@@ -24,4 +26,9 @@ program
|
|
|
24
26
|
.description('Show current authentication status')
|
|
25
27
|
.option('--staging', 'Use staging environment')
|
|
26
28
|
.action(status_js_1.statusCommand);
|
|
29
|
+
program
|
|
30
|
+
.command('upgrade')
|
|
31
|
+
.description('Upgrade to Premium and refresh your session')
|
|
32
|
+
.option('--staging', 'Use staging environment')
|
|
33
|
+
.action(upgrade_js_1.upgradeCommand);
|
|
27
34
|
program.parse();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface McpConfig {
|
|
2
|
+
refreshToken?: string;
|
|
3
|
+
environment?: 'production' | 'staging';
|
|
4
|
+
firebaseApiKey?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function saveConfig(config: McpConfig): void;
|
|
7
|
+
export declare function loadConfig(): McpConfig;
|
|
8
|
+
export declare function getRefreshTokenFromFile(): string | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.saveConfig = saveConfig;
|
|
7
|
+
exports.loadConfig = loadConfig;
|
|
8
|
+
exports.getRefreshTokenFromFile = getRefreshTokenFromFile;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.cutline-mcp');
|
|
13
|
+
const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
|
|
14
|
+
function ensureConfigDir() {
|
|
15
|
+
if (!fs_1.default.existsSync(CONFIG_DIR)) {
|
|
16
|
+
fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function saveConfig(config) {
|
|
20
|
+
ensureConfigDir();
|
|
21
|
+
const current = loadConfig();
|
|
22
|
+
const newConfig = { ...current, ...config };
|
|
23
|
+
fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
|
|
24
|
+
}
|
|
25
|
+
function loadConfig() {
|
|
26
|
+
try {
|
|
27
|
+
if (fs_1.default.existsSync(CONFIG_FILE)) {
|
|
28
|
+
const content = fs_1.default.readFileSync(CONFIG_FILE, 'utf-8');
|
|
29
|
+
return JSON.parse(content);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
// Ignore errors, return empty config
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
function getRefreshTokenFromFile() {
|
|
38
|
+
const config = loadConfig();
|
|
39
|
+
return config.refreshToken || null;
|
|
40
|
+
}
|
package/dist/utils/config.js
CHANGED
|
@@ -6,12 +6,16 @@ function getConfig(options = {}) {
|
|
|
6
6
|
return {
|
|
7
7
|
AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://cutline-staging.web.app/mcp-auth',
|
|
8
8
|
CALLBACK_URL: 'http://localhost:8765',
|
|
9
|
-
|
|
9
|
+
// For environment-scoped secrets, use FIREBASE_API_KEY (GitHub Actions will scope it to staging environment)
|
|
10
|
+
// Also support NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config
|
|
11
|
+
FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY || 'AIzaSyAAqU_euGAMtJoXp0sECblAIndifCp0pmE',
|
|
10
12
|
};
|
|
11
13
|
}
|
|
12
14
|
return {
|
|
13
15
|
AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://thecutline.ai/mcp-auth',
|
|
14
16
|
CALLBACK_URL: 'http://localhost:8765',
|
|
15
|
-
|
|
17
|
+
// For environment-scoped secrets, use FIREBASE_API_KEY (GitHub Actions will scope it to production environment)
|
|
18
|
+
// Also support NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config
|
|
19
|
+
FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY || 'AIzaSyAYBQt7A-a_mxn5SuXPHUpyIlm0eCn42N8',
|
|
16
20
|
};
|
|
17
21
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kylewadegrove/cutline-mcp-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI tool for authenticating with Cutline MCP servers",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -39,4 +39,4 @@
|
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|
|
41
41
|
}
|
|
42
|
-
}
|
|
42
|
+
}
|
package/src/commands/login.ts
CHANGED
|
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { startCallbackServer } from '../auth/callback.js';
|
|
5
5
|
import { storeRefreshToken } from '../auth/keychain.js';
|
|
6
|
+
import { saveConfig } from '../utils/config-store.js';
|
|
6
7
|
import { getConfig } from '../utils/config.js';
|
|
7
8
|
|
|
8
9
|
async function exchangeCustomToken(customToken: string, apiKey: string): Promise<{ refreshToken: string; email?: string }> {
|
|
@@ -30,19 +31,24 @@ async function exchangeCustomToken(customToken: string, apiKey: string): Promise
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
export async function loginCommand(options: { staging?: boolean }) {
|
|
34
|
+
export async function loginCommand(options: { staging?: boolean; signup?: boolean }) {
|
|
34
35
|
const config = getConfig(options);
|
|
35
|
-
|
|
36
|
+
|
|
37
|
+
if (options.signup) {
|
|
38
|
+
console.log(chalk.bold('\nš Cutline MCP - Create Account\n'));
|
|
39
|
+
} else {
|
|
40
|
+
console.log(chalk.bold('\nš Cutline MCP Authentication\n'));
|
|
41
|
+
}
|
|
36
42
|
|
|
37
43
|
if (options.staging) {
|
|
38
44
|
console.log(chalk.yellow(' ā ļø Using STAGING environment\n'));
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
if (!config.FIREBASE_API_KEY) {
|
|
42
|
-
|
|
43
|
-
console.error(chalk.red(`Error: ${varName} environment variable is required.`));
|
|
48
|
+
console.error(chalk.red(`Error: FIREBASE_API_KEY or NEXT_PUBLIC_FIREBASE_API_KEY environment variable is required.`));
|
|
44
49
|
console.error(chalk.gray('Please set it before running this command:'));
|
|
45
|
-
console.error(chalk.cyan(` export
|
|
50
|
+
console.error(chalk.cyan(` export FIREBASE_API_KEY=AIzaSy...`));
|
|
51
|
+
console.error(chalk.gray(' (or use NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config)'));
|
|
46
52
|
process.exit(1);
|
|
47
53
|
}
|
|
48
54
|
|
|
@@ -54,9 +60,14 @@ export async function loginCommand(options: { staging?: boolean }) {
|
|
|
54
60
|
const serverPromise = startCallbackServer();
|
|
55
61
|
|
|
56
62
|
// Open browser
|
|
57
|
-
|
|
63
|
+
let authUrl = `${config.AUTH_URL}?callback=${encodeURIComponent(config.CALLBACK_URL)}`;
|
|
64
|
+
if (options.signup) {
|
|
65
|
+
authUrl += '&mode=signup';
|
|
66
|
+
}
|
|
58
67
|
await open(authUrl);
|
|
59
|
-
spinner.text =
|
|
68
|
+
spinner.text = options.signup
|
|
69
|
+
? 'Browser opened - please create your account'
|
|
70
|
+
: 'Browser opened - please sign in or create an account';
|
|
60
71
|
|
|
61
72
|
// Wait for callback with custom token
|
|
62
73
|
const result = await serverPromise;
|
|
@@ -66,10 +77,29 @@ export async function loginCommand(options: { staging?: boolean }) {
|
|
|
66
77
|
const { refreshToken, email } = await exchangeCustomToken(result.token, config.FIREBASE_API_KEY);
|
|
67
78
|
|
|
68
79
|
// Store refresh token
|
|
69
|
-
|
|
80
|
+
try {
|
|
81
|
+
await storeRefreshToken(refreshToken);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.warn(chalk.yellow(' ā ļø Could not save to Keychain (skipping)'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Always save to file config (cross-platform)
|
|
87
|
+
try {
|
|
88
|
+
saveConfig({
|
|
89
|
+
refreshToken,
|
|
90
|
+
environment: options.staging ? 'staging' : 'production',
|
|
91
|
+
firebaseApiKey: config.FIREBASE_API_KEY
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(chalk.red(' ā Failed to save config file:'), error);
|
|
95
|
+
}
|
|
70
96
|
|
|
71
97
|
spinner.succeed(chalk.green('Successfully authenticated!'));
|
|
72
98
|
|
|
99
|
+
// Show environment indicator
|
|
100
|
+
const envLabel = options.staging ? chalk.yellow('STAGING') : chalk.green('PRODUCTION');
|
|
101
|
+
console.log(chalk.gray(` Environment: ${envLabel}`));
|
|
102
|
+
|
|
73
103
|
if (email || result.email) {
|
|
74
104
|
console.log(chalk.gray(` Logged in as: ${email || result.email}`));
|
|
75
105
|
}
|
package/src/commands/status.ts
CHANGED
|
@@ -52,9 +52,8 @@ export async function statusCommand(options: { staging?: boolean }) {
|
|
|
52
52
|
const config = getConfig(options);
|
|
53
53
|
|
|
54
54
|
if (!config.FIREBASE_API_KEY) {
|
|
55
|
-
const varName = options.staging ? 'FIREBASE_API_KEY_STAGING' : 'FIREBASE_API_KEY';
|
|
56
55
|
spinner.fail(chalk.red('Configuration error'));
|
|
57
|
-
console.error(chalk.red(`
|
|
56
|
+
console.error(chalk.red(` FIREBASE_API_KEY or NEXT_PUBLIC_FIREBASE_API_KEY environment variable is required.`));
|
|
58
57
|
process.exit(1);
|
|
59
58
|
}
|
|
60
59
|
|
|
@@ -64,7 +63,7 @@ export async function statusCommand(options: { staging?: boolean }) {
|
|
|
64
63
|
|
|
65
64
|
// Initialize Firebase Admin with correct project ID
|
|
66
65
|
if (admin.apps.length === 0) {
|
|
67
|
-
const projectId = options.staging ? '
|
|
66
|
+
const projectId = options.staging ? 'cutline-staging' : 'cutline-prod';
|
|
68
67
|
admin.initializeApp({ projectId });
|
|
69
68
|
}
|
|
70
69
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import open from 'open';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { startCallbackServer } from '../auth/callback.js';
|
|
5
|
+
import { storeRefreshToken } from '../auth/keychain.js';
|
|
6
|
+
import { saveConfig } from '../utils/config-store.js';
|
|
7
|
+
import { getConfig } from '../utils/config.js';
|
|
8
|
+
|
|
9
|
+
async function exchangeCustomToken(customToken: string, apiKey: string): Promise<{ refreshToken: string; email?: string }> {
|
|
10
|
+
const response = await fetch(
|
|
11
|
+
`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`,
|
|
12
|
+
{
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
body: JSON.stringify({
|
|
16
|
+
token: customToken,
|
|
17
|
+
returnSecureToken: true,
|
|
18
|
+
}),
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
const error = await response.text();
|
|
24
|
+
throw new Error(`Failed to exchange custom token: ${error}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
return {
|
|
29
|
+
refreshToken: data.refreshToken,
|
|
30
|
+
email: data.email,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function upgradeCommand(options: { staging?: boolean }) {
|
|
35
|
+
const config = getConfig(options);
|
|
36
|
+
|
|
37
|
+
console.log(chalk.bold('\nā¬ļø Cutline MCP - Upgrade to Premium\n'));
|
|
38
|
+
|
|
39
|
+
if (options.staging) {
|
|
40
|
+
console.log(chalk.yellow(' ā ļø Using STAGING environment\n'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!config.FIREBASE_API_KEY) {
|
|
44
|
+
console.error(chalk.red(`Error: FIREBASE_API_KEY environment variable is required.`));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Determine upgrade URL based on environment
|
|
49
|
+
const baseUrl = options.staging
|
|
50
|
+
? 'https://cutline-staging.web.app'
|
|
51
|
+
: 'https://thecutline.ai';
|
|
52
|
+
|
|
53
|
+
console.log(chalk.gray(' Opening upgrade page in your browser...\n'));
|
|
54
|
+
console.log(chalk.dim(' After upgrading, your MCP session will be refreshed automatically.\n'));
|
|
55
|
+
|
|
56
|
+
const spinner = ora('Waiting for upgrade and re-authentication...').start();
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Start callback server for re-auth after upgrade
|
|
60
|
+
const serverPromise = startCallbackServer();
|
|
61
|
+
|
|
62
|
+
// Open upgrade page with callback for re-auth
|
|
63
|
+
// The upgrade page will redirect to mcp-auth after successful upgrade
|
|
64
|
+
const upgradeUrl = `${baseUrl}/upgrade?mcp_callback=${encodeURIComponent(config.CALLBACK_URL)}`;
|
|
65
|
+
await open(upgradeUrl);
|
|
66
|
+
|
|
67
|
+
spinner.text = 'Browser opened - complete your upgrade, then re-authenticate';
|
|
68
|
+
|
|
69
|
+
// Wait for callback with new token (after upgrade + re-auth)
|
|
70
|
+
const result = await serverPromise;
|
|
71
|
+
|
|
72
|
+
// Exchange custom token for refresh token
|
|
73
|
+
spinner.text = 'Refreshing your session...';
|
|
74
|
+
const { refreshToken, email } = await exchangeCustomToken(result.token, config.FIREBASE_API_KEY);
|
|
75
|
+
|
|
76
|
+
// Store refresh token
|
|
77
|
+
try {
|
|
78
|
+
await storeRefreshToken(refreshToken);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.warn(chalk.yellow(' ā ļø Could not save to Keychain (skipping)'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Save to file config
|
|
84
|
+
try {
|
|
85
|
+
saveConfig({
|
|
86
|
+
refreshToken,
|
|
87
|
+
environment: options.staging ? 'staging' : 'production',
|
|
88
|
+
firebaseApiKey: config.FIREBASE_API_KEY
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error(chalk.red(' ā Failed to save config file:'), error);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
spinner.succeed(chalk.green('Upgrade complete! Session refreshed.'));
|
|
95
|
+
|
|
96
|
+
const envLabel = options.staging ? chalk.yellow('STAGING') : chalk.green('PRODUCTION');
|
|
97
|
+
console.log(chalk.gray(` Environment: ${envLabel}`));
|
|
98
|
+
|
|
99
|
+
if (email || result.email) {
|
|
100
|
+
console.log(chalk.gray(` Account: ${email || result.email}`));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(chalk.green('\n ⨠Premium features are now available!\n'));
|
|
104
|
+
console.log(chalk.dim(' Try:'), chalk.cyan('exploration_graduate'), chalk.dim('to run a full Deep Dive\n'));
|
|
105
|
+
|
|
106
|
+
} catch (error) {
|
|
107
|
+
spinner.fail(chalk.red('Upgrade flow failed'));
|
|
108
|
+
|
|
109
|
+
if (error instanceof Error) {
|
|
110
|
+
console.error(chalk.red(` ${error.message}`));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(chalk.gray('\n You can also upgrade at:'), chalk.cyan(`${baseUrl}/upgrade`));
|
|
114
|
+
console.log(chalk.gray(' Then run:'), chalk.cyan('cutline-mcp login'), chalk.gray('to refresh your session\n'));
|
|
115
|
+
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { loginCommand } from './commands/login.js';
|
|
4
4
|
import { logoutCommand } from './commands/logout.js';
|
|
5
5
|
import { statusCommand } from './commands/status.js';
|
|
6
|
+
import { upgradeCommand } from './commands/upgrade.js';
|
|
6
7
|
|
|
7
8
|
const program = new Command();
|
|
8
9
|
|
|
@@ -15,6 +16,7 @@ program
|
|
|
15
16
|
.command('login')
|
|
16
17
|
.description('Authenticate with Cutline and store credentials')
|
|
17
18
|
.option('--staging', 'Use staging environment')
|
|
19
|
+
.option('--signup', 'Open sign-up page instead of sign-in')
|
|
18
20
|
.action(loginCommand);
|
|
19
21
|
|
|
20
22
|
program
|
|
@@ -28,4 +30,10 @@ program
|
|
|
28
30
|
.option('--staging', 'Use staging environment')
|
|
29
31
|
.action(statusCommand);
|
|
30
32
|
|
|
33
|
+
program
|
|
34
|
+
.command('upgrade')
|
|
35
|
+
.description('Upgrade to Premium and refresh your session')
|
|
36
|
+
.option('--staging', 'Use staging environment')
|
|
37
|
+
.action(upgradeCommand);
|
|
38
|
+
|
|
31
39
|
program.parse();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = path.join(os.homedir(), '.cutline-mcp');
|
|
7
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
8
|
+
|
|
9
|
+
export interface McpConfig {
|
|
10
|
+
refreshToken?: string;
|
|
11
|
+
environment?: 'production' | 'staging';
|
|
12
|
+
firebaseApiKey?: string; // Store the API key used during login for reference
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ensureConfigDir() {
|
|
16
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
17
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function saveConfig(config: McpConfig) {
|
|
22
|
+
ensureConfigDir();
|
|
23
|
+
const current = loadConfig();
|
|
24
|
+
const newConfig = { ...current, ...config };
|
|
25
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loadConfig(): McpConfig {
|
|
29
|
+
try {
|
|
30
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
31
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// Ignore errors, return empty config
|
|
36
|
+
}
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getRefreshTokenFromFile(): string | null {
|
|
41
|
+
const config = loadConfig();
|
|
42
|
+
return config.refreshToken || null;
|
|
43
|
+
}
|
package/src/utils/config.ts
CHANGED
|
@@ -9,13 +9,17 @@ export function getConfig(options: { staging?: boolean } = {}): Config {
|
|
|
9
9
|
return {
|
|
10
10
|
AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://cutline-staging.web.app/mcp-auth',
|
|
11
11
|
CALLBACK_URL: 'http://localhost:8765',
|
|
12
|
-
|
|
12
|
+
// For environment-scoped secrets, use FIREBASE_API_KEY (GitHub Actions will scope it to staging environment)
|
|
13
|
+
// Also support NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config
|
|
14
|
+
FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY || 'AIzaSyAAqU_euGAMtJoXp0sECblAIndifCp0pmE',
|
|
13
15
|
};
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
return {
|
|
17
19
|
AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://thecutline.ai/mcp-auth',
|
|
18
20
|
CALLBACK_URL: 'http://localhost:8765',
|
|
19
|
-
|
|
21
|
+
// For environment-scoped secrets, use FIREBASE_API_KEY (GitHub Actions will scope it to production environment)
|
|
22
|
+
// Also support NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config
|
|
23
|
+
FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY || 'AIzaSyAYBQt7A-a_mxn5SuXPHUpyIlm0eCn42N8',
|
|
20
24
|
};
|
|
21
25
|
}
|