@kylewadegrove/cutline-mcp-cli 0.4.2 → 0.5.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/Dockerfile +11 -0
- package/README.md +177 -107
- package/dist/auth/callback.js +30 -32
- package/dist/auth/keychain.js +7 -15
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +246 -0
- package/dist/commands/login.js +39 -45
- package/dist/commands/logout.js +13 -19
- package/dist/commands/serve.d.ts +1 -0
- package/dist/commands/serve.js +38 -0
- package/dist/commands/setup.d.ts +5 -0
- package/dist/commands/setup.js +255 -0
- package/dist/commands/status.js +29 -35
- package/dist/commands/upgrade.js +44 -38
- package/dist/index.js +38 -14
- package/dist/servers/chunk-7FHM2GD3.js +5836 -0
- package/dist/servers/chunk-IVWF7VYZ.js +10086 -0
- package/dist/servers/chunk-JBJYSV4P.js +139 -0
- package/dist/servers/chunk-KMUSQOTJ.js +47 -0
- package/dist/servers/chunk-PD2HN2R5.js +908 -0
- package/dist/servers/chunk-PU7TL6S3.js +91 -0
- package/dist/servers/chunk-TGSEURMN.js +46 -0
- package/dist/servers/chunk-UBBAYTW3.js +946 -0
- package/dist/servers/cutline-server.js +11512 -0
- package/dist/servers/exploration-server.js +1030 -0
- package/dist/servers/graph-metrics-DCNR7JZN.js +12 -0
- package/dist/servers/integrations-server.js +121 -0
- package/dist/servers/output-server.js +120 -0
- package/dist/servers/pipeline-O5GJPNR4.js +20 -0
- package/dist/servers/premortem-handoff-XT4K3YDJ.js +10 -0
- package/dist/servers/premortem-server.js +958 -0
- package/dist/servers/score-history-HO5KRVGC.js +6 -0
- package/dist/servers/tools-server.js +291 -0
- package/dist/utils/config-store.js +13 -21
- package/dist/utils/config.js +2 -6
- package/mcpb/manifest.json +77 -0
- package/package.json +55 -9
- package/server.json +42 -0
- package/smithery.yaml +10 -0
- package/src/auth/callback.ts +0 -102
- package/src/auth/keychain.ts +0 -16
- package/src/commands/login.ts +0 -202
- package/src/commands/logout.ts +0 -30
- package/src/commands/status.ts +0 -153
- package/src/commands/upgrade.ts +0 -121
- package/src/index.ts +0 -40
- package/src/utils/config-store.ts +0 -46
- package/src/utils/config.ts +0 -65
- package/tsconfig.json +0 -22
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join, dirname, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { getRefreshToken } from '../auth/keychain.js';
|
|
9
|
+
import { fetchFirebaseApiKey } from '../utils/config.js';
|
|
10
|
+
import { loginCommand } from './login.js';
|
|
11
|
+
import { initCommand } from './init.js';
|
|
12
|
+
function getCliVersion() {
|
|
13
|
+
try {
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(dirname(__filename), '..', '..', 'package.json'), 'utf-8'));
|
|
16
|
+
return pkg.version ?? 'unknown';
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return 'unknown';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const SERVER_NAMES = [
|
|
23
|
+
'constraints',
|
|
24
|
+
'premortem',
|
|
25
|
+
'exploration',
|
|
26
|
+
'tools',
|
|
27
|
+
'output',
|
|
28
|
+
'integrations',
|
|
29
|
+
];
|
|
30
|
+
async function detectTier(options) {
|
|
31
|
+
const refreshToken = await getRefreshToken();
|
|
32
|
+
if (!refreshToken)
|
|
33
|
+
return { tier: 'free' };
|
|
34
|
+
try {
|
|
35
|
+
const apiKey = await fetchFirebaseApiKey(options);
|
|
36
|
+
const response = await fetch(`https://securetoken.googleapis.com/v1/token?key=${apiKey}`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ grant_type: 'refresh_token', refresh_token: refreshToken }),
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok)
|
|
42
|
+
return { tier: 'free' };
|
|
43
|
+
const data = await response.json();
|
|
44
|
+
const idToken = data.id_token;
|
|
45
|
+
const payload = JSON.parse(Buffer.from(idToken.split('.')[1], 'base64').toString());
|
|
46
|
+
const baseUrl = options.staging
|
|
47
|
+
? 'https://us-central1-cutline-staging.cloudfunctions.net'
|
|
48
|
+
: 'https://us-central1-cutline-prod.cloudfunctions.net';
|
|
49
|
+
const subRes = await fetch(`${baseUrl}/mcpSubscriptionStatus`, {
|
|
50
|
+
headers: { Authorization: `Bearer ${idToken}` },
|
|
51
|
+
});
|
|
52
|
+
const sub = subRes.ok ? await subRes.json() : { status: 'free' };
|
|
53
|
+
const isPremium = sub.status === 'active' || sub.status === 'trialing';
|
|
54
|
+
return { tier: isPremium ? 'premium' : 'free', email: payload.email, idToken };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { tier: 'free' };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function fetchProducts(idToken, options) {
|
|
61
|
+
try {
|
|
62
|
+
const baseUrl = options.staging
|
|
63
|
+
? 'https://us-central1-cutline-staging.cloudfunctions.net'
|
|
64
|
+
: 'https://us-central1-cutline-prod.cloudfunctions.net';
|
|
65
|
+
const res = await fetch(`${baseUrl}/mcpListProducts`, {
|
|
66
|
+
headers: { Authorization: `Bearer ${idToken}` },
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok)
|
|
69
|
+
return [];
|
|
70
|
+
const data = await res.json();
|
|
71
|
+
return data.products ?? [];
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function prompt(question) {
|
|
78
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
rl.question(question, (answer) => {
|
|
81
|
+
rl.close();
|
|
82
|
+
resolve(answer.trim());
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function buildServerConfig() {
|
|
87
|
+
const config = {};
|
|
88
|
+
for (const name of SERVER_NAMES) {
|
|
89
|
+
config[`cutline-${name}`] = {
|
|
90
|
+
command: 'npx',
|
|
91
|
+
args: ['-y', '@kylewadegrove/cutline-mcp-cli@latest', 'serve', name],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return config;
|
|
95
|
+
}
|
|
96
|
+
function mergeIdeConfig(filePath, serverConfig) {
|
|
97
|
+
let existing = {};
|
|
98
|
+
if (existsSync(filePath)) {
|
|
99
|
+
try {
|
|
100
|
+
existing = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
existing = {};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const dir = join(filePath, '..');
|
|
108
|
+
mkdirSync(dir, { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
const existingServers = (existing.mcpServers ?? {});
|
|
111
|
+
// Remove old cutline-* entries, then add fresh ones
|
|
112
|
+
const cleaned = {};
|
|
113
|
+
for (const [key, val] of Object.entries(existingServers)) {
|
|
114
|
+
if (!key.startsWith('cutline-')) {
|
|
115
|
+
cleaned[key] = val;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
existing.mcpServers = { ...cleaned, ...serverConfig };
|
|
119
|
+
writeFileSync(filePath, JSON.stringify(existing, null, 2) + '\n');
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
export async function setupCommand(options) {
|
|
123
|
+
const version = getCliVersion();
|
|
124
|
+
console.log(chalk.bold(`\n🔌 Cutline MCP Setup`) + chalk.dim(` v${version}\n`));
|
|
125
|
+
// ── 1. Authenticate ──────────────────────────────────────────────────────
|
|
126
|
+
const hasToken = await getRefreshToken();
|
|
127
|
+
if (!hasToken && !options.skipLogin) {
|
|
128
|
+
console.log(chalk.dim(' No credentials found — starting login flow.\n'));
|
|
129
|
+
await loginCommand({ staging: options.staging });
|
|
130
|
+
console.log();
|
|
131
|
+
}
|
|
132
|
+
const spinner = ora('Detecting account tier...').start();
|
|
133
|
+
const { tier, email, idToken } = await detectTier({ staging: options.staging });
|
|
134
|
+
if (email) {
|
|
135
|
+
spinner.succeed(chalk.green(`Authenticated as ${email} (${tier})`));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
spinner.succeed(chalk.yellow(`Running as ${tier} tier`));
|
|
139
|
+
}
|
|
140
|
+
console.log();
|
|
141
|
+
// ── 2. Connect to a product graph ────────────────────────────────────────
|
|
142
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
143
|
+
const configPath = join(projectRoot, '.cutline', 'config.json');
|
|
144
|
+
const hasExistingConfig = existsSync(configPath);
|
|
145
|
+
if (tier === 'premium' && idToken && !hasExistingConfig) {
|
|
146
|
+
const productSpinner = ora('Fetching your product graphs...').start();
|
|
147
|
+
const products = await fetchProducts(idToken, { staging: options.staging });
|
|
148
|
+
productSpinner.stop();
|
|
149
|
+
if (products.length > 0) {
|
|
150
|
+
console.log(chalk.bold(' Connect to a product graph\n'));
|
|
151
|
+
products.forEach((p, i) => {
|
|
152
|
+
const date = p.createdAt ? chalk.dim(` (${new Date(p.createdAt).toLocaleDateString()})`) : '';
|
|
153
|
+
console.log(` ${chalk.cyan(`${i + 1}.`)} ${chalk.white(p.name)}${date}`);
|
|
154
|
+
if (p.brief)
|
|
155
|
+
console.log(` ${chalk.dim(p.brief)}`);
|
|
156
|
+
});
|
|
157
|
+
console.log(` ${chalk.dim(`${products.length + 1}.`)} ${chalk.dim('Skip — I\'ll connect later')}`);
|
|
158
|
+
console.log();
|
|
159
|
+
const answer = await prompt(chalk.cyan(' Select a product (number): '));
|
|
160
|
+
const choice = parseInt(answer, 10);
|
|
161
|
+
if (choice >= 1 && choice <= products.length) {
|
|
162
|
+
const selected = products[choice - 1];
|
|
163
|
+
mkdirSync(join(projectRoot, '.cutline'), { recursive: true });
|
|
164
|
+
writeFileSync(configPath, JSON.stringify({
|
|
165
|
+
product_id: selected.id,
|
|
166
|
+
product_name: selected.name,
|
|
167
|
+
}, null, 2) + '\n');
|
|
168
|
+
console.log(chalk.green(`\n ✓ Connected to "${selected.name}"`));
|
|
169
|
+
console.log(chalk.dim(` ${configPath}\n`));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log(chalk.dim('\n Skipped. Run `cutline-mcp setup` again to connect later.\n'));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (hasExistingConfig) {
|
|
177
|
+
try {
|
|
178
|
+
const existing = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
179
|
+
console.log(chalk.green(` ✓ Connected to product graph:`), chalk.white(existing.product_name || existing.product_id));
|
|
180
|
+
console.log();
|
|
181
|
+
}
|
|
182
|
+
catch { /* ignore parse errors */ }
|
|
183
|
+
}
|
|
184
|
+
// ── 3. Write MCP server config to IDEs ───────────────────────────────────
|
|
185
|
+
const serverConfig = buildServerConfig();
|
|
186
|
+
const home = homedir();
|
|
187
|
+
const ideConfigs = [
|
|
188
|
+
{ name: 'Cursor', path: join(home, '.cursor', 'mcp.json') },
|
|
189
|
+
{ name: 'Claude Code', path: join(home, '.claude', 'settings.json') },
|
|
190
|
+
];
|
|
191
|
+
let wroteAny = false;
|
|
192
|
+
for (const ide of ideConfigs) {
|
|
193
|
+
// Write to Cursor always (primary target); write to Claude if dir exists
|
|
194
|
+
const dirExists = existsSync(join(ide.path, '..'));
|
|
195
|
+
if (ide.name === 'Cursor' || dirExists) {
|
|
196
|
+
try {
|
|
197
|
+
mergeIdeConfig(ide.path, serverConfig);
|
|
198
|
+
console.log(chalk.green(` ✓ ${ide.name}`), chalk.dim(ide.path));
|
|
199
|
+
wroteAny = true;
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
console.log(chalk.red(` ✗ ${ide.name}`), chalk.dim(err.message));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (wroteAny) {
|
|
207
|
+
console.log(chalk.dim('\n MCP server entries merged into IDE config (existing servers preserved).\n'));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
console.log(chalk.yellow('\n No IDE config files found. Printing config for manual setup:\n'));
|
|
211
|
+
console.log(chalk.green(JSON.stringify({ mcpServers: serverConfig }, null, 2)));
|
|
212
|
+
console.log();
|
|
213
|
+
}
|
|
214
|
+
// ── 4. Generate IDE rules ────────────────────────────────────────────────
|
|
215
|
+
console.log(chalk.bold(' Generating IDE rules...\n'));
|
|
216
|
+
await initCommand({ projectRoot: options.projectRoot, staging: options.staging });
|
|
217
|
+
// ── 5. Claude Code one-liners ────────────────────────────────────────────
|
|
218
|
+
console.log(chalk.bold(' Claude Code one-liner alternative:\n'));
|
|
219
|
+
console.log(chalk.dim(' If you prefer `claude mcp add` instead of settings.json:\n'));
|
|
220
|
+
const coreServers = ['constraints', 'premortem', 'tools', 'exploration'];
|
|
221
|
+
for (const name of coreServers) {
|
|
222
|
+
console.log(chalk.cyan(` claude mcp add cutline-${name} -- npx -y @kylewadegrove/cutline-mcp-cli serve ${name}`));
|
|
223
|
+
}
|
|
224
|
+
console.log();
|
|
225
|
+
// ── 6. What you can do ───────────────────────────────────────────────────
|
|
226
|
+
console.log(chalk.bold(' Restart your IDE, then ask your AI agent:\n'));
|
|
227
|
+
if (tier === 'premium') {
|
|
228
|
+
const items = [
|
|
229
|
+
{ cmd: 'Run a deep dive on my product idea', desc: 'Pre-mortem analysis — risks, assumptions, experiments' },
|
|
230
|
+
{ cmd: 'Run a code audit for my product', desc: 'Security scan + RGR remediation plan' },
|
|
231
|
+
{ cmd: 'Check constraints for src/api/upload.ts', desc: 'Get NFR boundaries for a specific file' },
|
|
232
|
+
{ cmd: 'Generate .cutline.md for my product', desc: 'Write the constraint routing engine' },
|
|
233
|
+
{ cmd: 'What does my persona think about X?', desc: 'AI persona feedback on features' },
|
|
234
|
+
];
|
|
235
|
+
for (const item of items) {
|
|
236
|
+
console.log(` ${chalk.cyan('→')} ${chalk.white(`"${item.cmd}"`)}`);
|
|
237
|
+
console.log(` ${chalk.dim(item.desc)}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const items = [
|
|
242
|
+
{ cmd: 'Run an engineering audit on this codebase', desc: 'Security, reliability, and scalability scan (3/month free)' },
|
|
243
|
+
];
|
|
244
|
+
for (const item of items) {
|
|
245
|
+
console.log(` ${chalk.cyan('→')} ${chalk.white(`"${item.cmd}"`)}`);
|
|
246
|
+
console.log(` ${chalk.dim(item.desc)}`);
|
|
247
|
+
}
|
|
248
|
+
console.log();
|
|
249
|
+
console.log(chalk.dim(' Upgrade to Premium for deep dives, code audits, constraint graphs, and personas'));
|
|
250
|
+
console.log(chalk.dim(' →'), chalk.cyan('cutline-mcp upgrade'), chalk.dim('or https://thecutline.ai/upgrade'));
|
|
251
|
+
}
|
|
252
|
+
console.log();
|
|
253
|
+
console.log(chalk.dim(` cutline-mcp v${version} · docs: https://thecutline.ai/docs/setup`));
|
|
254
|
+
console.log();
|
|
255
|
+
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.statusCommand = statusCommand;
|
|
7
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
-
const ora_1 = __importDefault(require("ora"));
|
|
9
|
-
const keychain_js_1 = require("../auth/keychain.js");
|
|
10
|
-
const config_js_1 = require("../utils/config.js");
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { getRefreshToken } from '../auth/keychain.js';
|
|
4
|
+
import { fetchFirebaseApiKey } from '../utils/config.js';
|
|
11
5
|
async function getSubscriptionStatus(idToken, isStaging) {
|
|
12
6
|
try {
|
|
13
7
|
const baseUrl = isStaging
|
|
@@ -53,26 +47,26 @@ async function exchangeRefreshToken(refreshToken, apiKey) {
|
|
|
53
47
|
const data = await response.json();
|
|
54
48
|
return data.id_token;
|
|
55
49
|
}
|
|
56
|
-
async function statusCommand(options) {
|
|
57
|
-
console.log(
|
|
58
|
-
const spinner = (
|
|
50
|
+
export async function statusCommand(options) {
|
|
51
|
+
console.log(chalk.bold('\n📊 Cutline MCP Status\n'));
|
|
52
|
+
const spinner = ora('Checking authentication...').start();
|
|
59
53
|
try {
|
|
60
54
|
// Check for stored refresh token
|
|
61
|
-
const refreshToken = await
|
|
55
|
+
const refreshToken = await getRefreshToken();
|
|
62
56
|
if (!refreshToken) {
|
|
63
|
-
spinner.info(
|
|
64
|
-
console.log(
|
|
57
|
+
spinner.info(chalk.yellow('Not authenticated'));
|
|
58
|
+
console.log(chalk.gray(' Run'), chalk.cyan('cutline-mcp login'), chalk.gray('to authenticate\n'));
|
|
65
59
|
return;
|
|
66
60
|
}
|
|
67
61
|
// Get Firebase API key
|
|
68
62
|
spinner.text = 'Fetching configuration...';
|
|
69
63
|
let firebaseApiKey;
|
|
70
64
|
try {
|
|
71
|
-
firebaseApiKey = await
|
|
65
|
+
firebaseApiKey = await fetchFirebaseApiKey(options);
|
|
72
66
|
}
|
|
73
67
|
catch (error) {
|
|
74
|
-
spinner.fail(
|
|
75
|
-
console.error(
|
|
68
|
+
spinner.fail(chalk.red('Configuration error'));
|
|
69
|
+
console.error(chalk.red(` ${error instanceof Error ? error.message : 'Failed to get Firebase API key'}`));
|
|
76
70
|
process.exit(1);
|
|
77
71
|
}
|
|
78
72
|
// Exchange refresh token for ID token
|
|
@@ -81,18 +75,18 @@ async function statusCommand(options) {
|
|
|
81
75
|
// Decode JWT payload (base64) to get user info - no verification needed, just display
|
|
82
76
|
const payloadBase64 = idToken.split('.')[1];
|
|
83
77
|
const decoded = JSON.parse(Buffer.from(payloadBase64, 'base64').toString());
|
|
84
|
-
spinner.succeed(
|
|
85
|
-
console.log(
|
|
86
|
-
console.log(
|
|
78
|
+
spinner.succeed(chalk.green('Authenticated'));
|
|
79
|
+
console.log(chalk.gray(' User:'), chalk.white(decoded.email || decoded.user_id || decoded.sub));
|
|
80
|
+
console.log(chalk.gray(' UID:'), chalk.dim(decoded.user_id || decoded.sub));
|
|
87
81
|
// Calculate token expiry
|
|
88
82
|
const expiresIn = Math.floor((decoded.exp * 1000 - Date.now()) / 1000 / 60);
|
|
89
|
-
console.log(
|
|
83
|
+
console.log(chalk.gray(' Token expires in:'), chalk.white(`${expiresIn} minutes`));
|
|
90
84
|
// Show custom claims if present
|
|
91
85
|
if (decoded.mcp) {
|
|
92
|
-
console.log(
|
|
86
|
+
console.log(chalk.gray(' MCP enabled:'), chalk.green('✓'));
|
|
93
87
|
}
|
|
94
88
|
if (decoded.deviceId) {
|
|
95
|
-
console.log(
|
|
89
|
+
console.log(chalk.gray(' Device ID:'), chalk.dim(decoded.deviceId));
|
|
96
90
|
}
|
|
97
91
|
// Check subscription status via Cloud Function
|
|
98
92
|
spinner.start('Checking subscription...');
|
|
@@ -100,33 +94,33 @@ async function statusCommand(options) {
|
|
|
100
94
|
spinner.stop();
|
|
101
95
|
if (subscription.status === 'active' || subscription.status === 'trialing') {
|
|
102
96
|
const statusLabel = subscription.status === 'trialing' ? ' (trial)' : '';
|
|
103
|
-
console.log(
|
|
97
|
+
console.log(chalk.gray(' Plan:'), chalk.green(`✓ ${subscription.planName || 'Premium'}${statusLabel}`));
|
|
104
98
|
if (subscription.periodEnd) {
|
|
105
99
|
const periodEndDate = new Date(subscription.periodEnd);
|
|
106
100
|
const daysLeft = Math.ceil((periodEndDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
107
|
-
console.log(
|
|
101
|
+
console.log(chalk.gray(' Renews:'), chalk.white(`${periodEndDate.toLocaleDateString()} (${daysLeft} days)`));
|
|
108
102
|
}
|
|
109
103
|
}
|
|
110
104
|
else if (subscription.status === 'past_due') {
|
|
111
|
-
console.log(
|
|
105
|
+
console.log(chalk.gray(' Plan:'), chalk.yellow('⚠ Premium (payment past due)'));
|
|
112
106
|
}
|
|
113
107
|
else if (subscription.status === 'canceled') {
|
|
114
|
-
console.log(
|
|
108
|
+
console.log(chalk.gray(' Plan:'), chalk.yellow('Premium (canceled)'));
|
|
115
109
|
if (subscription.periodEnd) {
|
|
116
|
-
console.log(
|
|
110
|
+
console.log(chalk.gray(' Access until:'), chalk.white(new Date(subscription.periodEnd).toLocaleDateString()));
|
|
117
111
|
}
|
|
118
112
|
}
|
|
119
113
|
else {
|
|
120
|
-
console.log(
|
|
121
|
-
console.log(
|
|
114
|
+
console.log(chalk.gray(' Plan:'), chalk.white('Free'));
|
|
115
|
+
console.log(chalk.dim(' Upgrade at'), chalk.cyan('https://thecutline.ai/pricing'));
|
|
122
116
|
}
|
|
123
117
|
console.log();
|
|
124
118
|
}
|
|
125
119
|
catch (error) {
|
|
126
|
-
spinner.fail(
|
|
120
|
+
spinner.fail(chalk.red('Status check failed'));
|
|
127
121
|
if (error instanceof Error) {
|
|
128
|
-
console.error(
|
|
129
|
-
console.log(
|
|
122
|
+
console.error(chalk.red(` ${error.message}`));
|
|
123
|
+
console.log(chalk.gray(' Try running'), chalk.cyan('cutline-mcp login'), chalk.gray('again\n'));
|
|
130
124
|
}
|
|
131
125
|
process.exit(1);
|
|
132
126
|
}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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");
|
|
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, fetchFirebaseApiKey } from '../utils/config.js';
|
|
14
8
|
async function exchangeCustomToken(customToken, apiKey) {
|
|
15
9
|
const response = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`, {
|
|
16
10
|
method: 'POST',
|
|
@@ -30,35 +24,35 @@ async function exchangeCustomToken(customToken, apiKey) {
|
|
|
30
24
|
email: data.email,
|
|
31
25
|
};
|
|
32
26
|
}
|
|
33
|
-
async function upgradeCommand(options) {
|
|
34
|
-
const config =
|
|
35
|
-
console.log(
|
|
27
|
+
export async function upgradeCommand(options) {
|
|
28
|
+
const config = getConfig(options);
|
|
29
|
+
console.log(chalk.bold('\n⬆️ Cutline MCP - Upgrade to Premium\n'));
|
|
36
30
|
if (options.staging) {
|
|
37
|
-
console.log(
|
|
31
|
+
console.log(chalk.yellow(' ⚠️ Using STAGING environment\n'));
|
|
38
32
|
}
|
|
39
33
|
// Fetch Firebase API key
|
|
40
34
|
let firebaseApiKey;
|
|
41
35
|
try {
|
|
42
|
-
firebaseApiKey = await
|
|
36
|
+
firebaseApiKey = await fetchFirebaseApiKey(options);
|
|
43
37
|
}
|
|
44
38
|
catch (error) {
|
|
45
|
-
console.error(
|
|
39
|
+
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : 'Failed to get Firebase API key'}`));
|
|
46
40
|
process.exit(1);
|
|
47
41
|
}
|
|
48
42
|
// Determine upgrade URL based on environment
|
|
49
43
|
const baseUrl = options.staging
|
|
50
44
|
? 'https://cutline-staging.web.app'
|
|
51
45
|
: 'https://thecutline.ai';
|
|
52
|
-
console.log(
|
|
53
|
-
console.log(
|
|
54
|
-
const spinner = (
|
|
46
|
+
console.log(chalk.gray(' Opening upgrade page in your browser...\n'));
|
|
47
|
+
console.log(chalk.dim(' After upgrading, your MCP session will be refreshed automatically.\n'));
|
|
48
|
+
const spinner = ora('Waiting for upgrade and re-authentication...').start();
|
|
55
49
|
try {
|
|
56
50
|
// Start callback server for re-auth after upgrade
|
|
57
|
-
const serverPromise =
|
|
51
|
+
const serverPromise = startCallbackServer();
|
|
58
52
|
// Open upgrade page with callback for re-auth
|
|
59
53
|
// The upgrade page will redirect to mcp-auth after successful upgrade
|
|
60
54
|
const upgradeUrl = `${baseUrl}/upgrade?mcp_callback=${encodeURIComponent(config.CALLBACK_URL)}`;
|
|
61
|
-
await (
|
|
55
|
+
await open(upgradeUrl);
|
|
62
56
|
spinner.text = 'Browser opened - complete your upgrade, then re-authenticate';
|
|
63
57
|
// Wait for callback with new token (after upgrade + re-auth)
|
|
64
58
|
const result = await serverPromise;
|
|
@@ -67,37 +61,49 @@ async function upgradeCommand(options) {
|
|
|
67
61
|
const { refreshToken, email } = await exchangeCustomToken(result.token, firebaseApiKey);
|
|
68
62
|
// Store refresh token
|
|
69
63
|
try {
|
|
70
|
-
await
|
|
64
|
+
await storeRefreshToken(refreshToken);
|
|
71
65
|
}
|
|
72
66
|
catch (error) {
|
|
73
|
-
console.warn(
|
|
67
|
+
console.warn(chalk.yellow(' ⚠️ Could not save to Keychain (skipping)'));
|
|
74
68
|
}
|
|
75
69
|
// Save to file config (API key is fetched at runtime, not stored)
|
|
76
70
|
try {
|
|
77
|
-
|
|
71
|
+
saveConfig({
|
|
78
72
|
refreshToken,
|
|
79
73
|
environment: options.staging ? 'staging' : 'production',
|
|
80
74
|
});
|
|
81
75
|
}
|
|
82
76
|
catch (error) {
|
|
83
|
-
console.error(
|
|
77
|
+
console.error(chalk.red(' ✗ Failed to save config file:'), error);
|
|
84
78
|
}
|
|
85
|
-
spinner.succeed(
|
|
86
|
-
const envLabel = options.staging ?
|
|
87
|
-
console.log(
|
|
79
|
+
spinner.succeed(chalk.green('Upgrade complete! Session refreshed.'));
|
|
80
|
+
const envLabel = options.staging ? chalk.yellow('STAGING') : chalk.green('PRODUCTION');
|
|
81
|
+
console.log(chalk.gray(` Environment: ${envLabel}`));
|
|
88
82
|
if (email || result.email) {
|
|
89
|
-
console.log(
|
|
83
|
+
console.log(chalk.gray(` Account: ${email || result.email}`));
|
|
90
84
|
}
|
|
91
|
-
console.log(
|
|
92
|
-
console.log(
|
|
85
|
+
console.log(chalk.green('\n Premium features are now available!\n'));
|
|
86
|
+
console.log(chalk.bold(' Re-run init to update your IDE rules:'));
|
|
87
|
+
console.log(chalk.cyan(' cutline-mcp init\n'));
|
|
88
|
+
console.log(chalk.bold(' Then ask your AI agent:\n'));
|
|
89
|
+
const items = [
|
|
90
|
+
{ cmd: 'Run a deep dive on my product idea', desc: 'Pre-mortem analysis — risks, assumptions, experiments' },
|
|
91
|
+
{ cmd: 'Run a code audit for my product', desc: 'Security scan + RGR remediation plan' },
|
|
92
|
+
{ cmd: 'Generate .cutline.md for my product', desc: 'Write the constraint routing engine' },
|
|
93
|
+
];
|
|
94
|
+
for (const item of items) {
|
|
95
|
+
console.log(` ${chalk.cyan('→')} ${chalk.white(`"${item.cmd}"`)}`);
|
|
96
|
+
console.log(` ${chalk.dim(item.desc)}`);
|
|
97
|
+
}
|
|
98
|
+
console.log();
|
|
93
99
|
}
|
|
94
100
|
catch (error) {
|
|
95
|
-
spinner.fail(
|
|
101
|
+
spinner.fail(chalk.red('Upgrade flow failed'));
|
|
96
102
|
if (error instanceof Error) {
|
|
97
|
-
console.error(
|
|
103
|
+
console.error(chalk.red(` ${error.message}`));
|
|
98
104
|
}
|
|
99
|
-
console.log(
|
|
100
|
-
console.log(
|
|
105
|
+
console.log(chalk.gray('\n You can also upgrade at:'), chalk.cyan(`${baseUrl}/upgrade`));
|
|
106
|
+
console.log(chalk.gray(' Then run:'), chalk.cyan('cutline-mcp login'), chalk.gray('to refresh your session\n'));
|
|
101
107
|
process.exit(1);
|
|
102
108
|
}
|
|
103
109
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,35 +1,59 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
import { loginCommand } from './commands/login.js';
|
|
7
|
+
import { logoutCommand } from './commands/logout.js';
|
|
8
|
+
import { statusCommand } from './commands/status.js';
|
|
9
|
+
import { upgradeCommand } from './commands/upgrade.js';
|
|
10
|
+
import { serveCommand } from './commands/serve.js';
|
|
11
|
+
import { setupCommand } from './commands/setup.js';
|
|
12
|
+
import { initCommand } from './commands/init.js';
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
16
|
+
const program = new Command();
|
|
10
17
|
program
|
|
11
18
|
.name('cutline-mcp')
|
|
12
|
-
.description('CLI
|
|
13
|
-
.version(
|
|
19
|
+
.description('CLI and MCP servers for Cutline')
|
|
20
|
+
.version(pkg.version);
|
|
14
21
|
program
|
|
15
22
|
.command('login')
|
|
16
23
|
.description('Authenticate with Cutline and store credentials')
|
|
17
24
|
.option('--staging', 'Use staging environment')
|
|
18
25
|
.option('--signup', 'Open sign-up page instead of sign-in')
|
|
19
26
|
.option('--email <address>', 'Request sign-in with specific email address')
|
|
20
|
-
.action(
|
|
27
|
+
.action(loginCommand);
|
|
21
28
|
program
|
|
22
29
|
.command('logout')
|
|
23
30
|
.description('Remove stored credentials')
|
|
24
|
-
.action(
|
|
31
|
+
.action(logoutCommand);
|
|
25
32
|
program
|
|
26
33
|
.command('status')
|
|
27
34
|
.description('Show current authentication status')
|
|
28
35
|
.option('--staging', 'Use staging environment')
|
|
29
|
-
.action(
|
|
36
|
+
.action(statusCommand);
|
|
30
37
|
program
|
|
31
38
|
.command('upgrade')
|
|
32
39
|
.description('Upgrade to Premium and refresh your session')
|
|
33
40
|
.option('--staging', 'Use staging environment')
|
|
34
|
-
.action(
|
|
41
|
+
.action(upgradeCommand);
|
|
42
|
+
program
|
|
43
|
+
.command('serve <server>')
|
|
44
|
+
.description('Start an MCP server (constraints, premortem, exploration, tools, output, integrations)')
|
|
45
|
+
.action(serveCommand);
|
|
46
|
+
program
|
|
47
|
+
.command('setup')
|
|
48
|
+
.description('One-command onboarding: authenticate, write IDE MCP config, generate rules')
|
|
49
|
+
.option('--staging', 'Use staging environment')
|
|
50
|
+
.option('--skip-login', 'Skip authentication (use existing credentials)')
|
|
51
|
+
.option('--project-root <path>', 'Project root directory for IDE rules (default: cwd)')
|
|
52
|
+
.action((opts) => setupCommand({ staging: opts.staging, skipLogin: opts.skipLogin, projectRoot: opts.projectRoot }));
|
|
53
|
+
program
|
|
54
|
+
.command('init')
|
|
55
|
+
.description('Generate IDE rules only (setup runs this automatically)')
|
|
56
|
+
.option('--project-root <path>', 'Project root directory (default: cwd)')
|
|
57
|
+
.option('--staging', 'Use staging environment')
|
|
58
|
+
.action((opts) => initCommand({ projectRoot: opts.projectRoot, staging: opts.staging }));
|
|
35
59
|
program.parse();
|