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