@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,309 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { resolve, join } from 'node:path';
|
|
5
|
+
import { getRefreshToken } from '../auth/keychain.js';
|
|
6
|
+
import { fetchFirebaseApiKey } from '../utils/config.js';
|
|
7
|
+
import { saveConfig, loadConfig } from '../utils/config-store.js';
|
|
8
|
+
const CUTLINE_CONFIG = '.cutline/config.json';
|
|
9
|
+
async function authenticate(options) {
|
|
10
|
+
const refreshToken = await getRefreshToken();
|
|
11
|
+
if (!refreshToken)
|
|
12
|
+
return null;
|
|
13
|
+
try {
|
|
14
|
+
const apiKey = await fetchFirebaseApiKey(options);
|
|
15
|
+
const response = await fetch(`https://securetoken.googleapis.com/v1/token?key=${apiKey}`, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
body: JSON.stringify({ grant_type: 'refresh_token', refresh_token: refreshToken }),
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok)
|
|
21
|
+
return null;
|
|
22
|
+
const data = await response.json();
|
|
23
|
+
const idToken = data.id_token;
|
|
24
|
+
const payload = JSON.parse(Buffer.from(idToken.split('.')[1], 'base64').toString());
|
|
25
|
+
const baseUrl = options.staging
|
|
26
|
+
? 'https://us-central1-cutline-staging.cloudfunctions.net'
|
|
27
|
+
: 'https://us-central1-cutline-prod.cloudfunctions.net';
|
|
28
|
+
const subRes = await fetch(`${baseUrl}/mcpSubscriptionStatus`, {
|
|
29
|
+
headers: { Authorization: `Bearer ${idToken}` },
|
|
30
|
+
});
|
|
31
|
+
const sub = subRes.ok ? await subRes.json() : { status: 'free' };
|
|
32
|
+
const isPremium = sub.status === 'active' || sub.status === 'trialing';
|
|
33
|
+
return {
|
|
34
|
+
tier: isPremium ? 'premium' : 'free',
|
|
35
|
+
email: payload.email,
|
|
36
|
+
uid: payload.user_id || payload.sub,
|
|
37
|
+
idToken,
|
|
38
|
+
baseUrl,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function readCutlineConfig(projectRoot) {
|
|
46
|
+
const configPath = join(projectRoot, CUTLINE_CONFIG);
|
|
47
|
+
if (!existsSync(configPath))
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function cursorRgrRule(config, tier) {
|
|
57
|
+
const productId = config?.product_id ?? '<from .cutline/config.json>';
|
|
58
|
+
const productName = config?.product_name ?? 'your product';
|
|
59
|
+
const verifyTool = tier === 'premium'
|
|
60
|
+
? `\`engineering_audit(product_id: "${productId}", project_root: "<workspace>")\``
|
|
61
|
+
: `\`code_audit(project_root: "<workspace>")\``;
|
|
62
|
+
const planStep = tier === 'premium'
|
|
63
|
+
? `1. **Plan**: \`rgr_plan(product_id: "${productId}", file_path: "<file>")\``
|
|
64
|
+
: `1. **Plan**: \`code_audit(project_root: "<workspace>")\` to identify top active issue`;
|
|
65
|
+
return `---
|
|
66
|
+
description: RGR development workflow using Cutline MCP tools
|
|
67
|
+
globs:
|
|
68
|
+
alwaysApply: true
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
# RGR-Driven Development
|
|
72
|
+
|
|
73
|
+
Product: **${productName}**
|
|
74
|
+
|
|
75
|
+
When Cutline MCP servers are connected, follow this workflow for feature implementations, bug fixes, or refactoring.
|
|
76
|
+
|
|
77
|
+
## The RGR Cycle
|
|
78
|
+
|
|
79
|
+
${planStep}
|
|
80
|
+
2. **Implement**: Address constraints/findings from the plan
|
|
81
|
+
3. **Verify**: ${verifyTool}
|
|
82
|
+
4. **Complete**: ${tier === 'premium' ? `\`rgr_complete_phase(product_id: "${productId}", phase: "<phase>")\`` : 'Re-scan to confirm scores improved'}
|
|
83
|
+
|
|
84
|
+
## When to use RGR
|
|
85
|
+
|
|
86
|
+
Always: New features, security/auth, billing, API endpoints, DB schema, infra.
|
|
87
|
+
Skip: Pure styling, docs, formatting, non-security dep bumps.
|
|
88
|
+
|
|
89
|
+
## Active vs Latent Issues
|
|
90
|
+
|
|
91
|
+
- **Active**: Problems in existing code. Fix now. (from \`code_audit\`, \`engineering_audit\`)
|
|
92
|
+
- **Latent**: Risks in planned features. Address as you build. (from deep dives, \`rgr_plan\`)
|
|
93
|
+
|
|
94
|
+
## Magic Phrase
|
|
95
|
+
|
|
96
|
+
If the user invokes Cutline naturally (for example: **"use cutline"**, **"use cutline to..."**, **"using cutline..."**, **"with cutline..."**), route automatically based on tier and intent:
|
|
97
|
+
- Free/default intent: run \`code_audit(project_root)\`
|
|
98
|
+
- Premium product-linked intent: run \`engineering_audit(product_id, project_root)\`
|
|
99
|
+
- Feature implementation intent: run \`rgr_plan(...)\` then \`constraints_auto(...)\`
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
function cursorConstraintsRule(config, tier) {
|
|
103
|
+
if (tier === 'free') {
|
|
104
|
+
return `---
|
|
105
|
+
description: Cutline engineering audit integration
|
|
106
|
+
globs:
|
|
107
|
+
alwaysApply: true
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
# Cutline Constraints
|
|
111
|
+
|
|
112
|
+
Run \`code_audit(project_root)\` before major implementations to check constraint coverage.
|
|
113
|
+
|
|
114
|
+
Severity levels:
|
|
115
|
+
- **CRITICAL**: Must address before proceeding
|
|
116
|
+
- **WARNING**: Consider in your approach
|
|
117
|
+
- **INFO**: Context for UX copy, error messages
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
120
|
+
const productId = config?.product_id ?? '<from .cutline/config.json>';
|
|
121
|
+
return `---
|
|
122
|
+
description: Proactive constraint checking from Cutline constraint graph
|
|
123
|
+
globs:
|
|
124
|
+
alwaysApply: true
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
# Cutline Constraints
|
|
128
|
+
|
|
129
|
+
Read \`.cutline/config.json\` to get product_id. Call \`constraints_auto\` when modifying sensitive paths:
|
|
130
|
+
|
|
131
|
+
\`\`\`
|
|
132
|
+
constraints_auto(product_id: "${productId}", file_paths: [<files>], task_description: "<task>", mode: "advisory")
|
|
133
|
+
\`\`\`
|
|
134
|
+
|
|
135
|
+
Check constraints when touching: auth, billing, security, AI/LLM, integrations, user-facing flows.
|
|
136
|
+
|
|
137
|
+
Severity levels:
|
|
138
|
+
- **CRITICAL**: Must address before proceeding
|
|
139
|
+
- **WARNING**: Consider in your approach
|
|
140
|
+
- **INFO**: Context for UX copy, error messages
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
function cursorCutlinePointer() {
|
|
144
|
+
return `---
|
|
145
|
+
description: Cutline constraint integration
|
|
146
|
+
globs:
|
|
147
|
+
alwaysApply: true
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
# Cutline Integration
|
|
151
|
+
|
|
152
|
+
Read \`.cutline.md\` before planning or executing ANY code in this repository.
|
|
153
|
+
If a rule in \`.cutline.md\` conflicts with a stylistic rule below, \`.cutline.md\` takes precedence.
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
function claudeLocalContent(config, tier) {
|
|
157
|
+
const productId = config?.product_id ?? '<product_id>';
|
|
158
|
+
const productName = config?.product_name ?? 'your product';
|
|
159
|
+
const verifyCmd = tier === 'premium'
|
|
160
|
+
? `engineering_audit(product_id: "${productId}", project_root)`
|
|
161
|
+
: `code_audit(project_root)`;
|
|
162
|
+
return `# Cutline Integration
|
|
163
|
+
|
|
164
|
+
This file is auto-generated by \`cutline-mcp init\`. Do not commit to version control.
|
|
165
|
+
|
|
166
|
+
Product: ${productName}${config?.product_id ? ` (ID: \`${config.product_id}\`)` : ''}
|
|
167
|
+
Tier: ${tier}
|
|
168
|
+
|
|
169
|
+
${tier === 'premium' ? `Read \`.cutline.md\` before planning or executing code in this repository.\n` : ''}## RGR Workflow
|
|
170
|
+
|
|
171
|
+
1. **Plan**: ${tier === 'premium' ? `\`rgr_plan(product_id: "${productId}", file_path)\`` : `\`code_audit(project_root)\``} before writing code
|
|
172
|
+
2. **Implement**: Address constraints/findings from the plan
|
|
173
|
+
3. **Verify**: \`${verifyCmd}\`
|
|
174
|
+
4. **Complete**: ${tier === 'premium' ? `\`rgr_complete_phase(product_id: "${productId}", phase)\`` : 'Re-scan to confirm scores improved'}
|
|
175
|
+
|
|
176
|
+
Use RGR for: new features, security/auth, billing, API endpoints, DB schema, infra.
|
|
177
|
+
Skip for: pure styling, docs, formatting, non-security dep bumps.
|
|
178
|
+
|
|
179
|
+
## Active vs Latent Issues
|
|
180
|
+
|
|
181
|
+
- **Active**: Problems in existing code. Fix now.
|
|
182
|
+
- **Latent**: Risks in planned features. Address as you build.
|
|
183
|
+
|
|
184
|
+
## Magic Phrase
|
|
185
|
+
|
|
186
|
+
If the user invokes Cutline naturally (for example: **"use cutline"**, **"use cutline to..."**, **"using cutline..."**, **"with cutline..."**), route automatically based on tier and intent:
|
|
187
|
+
- Free/default intent: \`code_audit(project_root)\`
|
|
188
|
+
- Premium product-linked intent: \`engineering_audit(product_id, project_root)\`
|
|
189
|
+
- Feature implementation intent: \`rgr_plan(...)\` then \`constraints_auto(...)\`
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
function ensureGitignore(projectRoot, patterns) {
|
|
193
|
+
const gitignorePath = join(projectRoot, '.gitignore');
|
|
194
|
+
if (!existsSync(gitignorePath))
|
|
195
|
+
return false;
|
|
196
|
+
let content = readFileSync(gitignorePath, 'utf-8');
|
|
197
|
+
const additions = patterns.filter((p) => !content.includes(p));
|
|
198
|
+
if (additions.length === 0)
|
|
199
|
+
return false;
|
|
200
|
+
content = content.trimEnd() + '\n\n# Cutline generated (re-run cutline-mcp init to update)\n' + additions.join('\n') + '\n';
|
|
201
|
+
writeFileSync(gitignorePath, content);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
export async function initCommand(options) {
|
|
205
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
206
|
+
const config = readCutlineConfig(projectRoot);
|
|
207
|
+
console.log(chalk.bold('\nš§ Cutline Init\n'));
|
|
208
|
+
// Authenticate and determine tier
|
|
209
|
+
const spinner = ora('Checking authentication...').start();
|
|
210
|
+
const auth = await authenticate({ staging: options.staging });
|
|
211
|
+
let tier = 'free';
|
|
212
|
+
if (!auth) {
|
|
213
|
+
spinner.warn(chalk.yellow('Not authenticated ā generating free-tier rules'));
|
|
214
|
+
console.log(chalk.dim(' Run `cutline-mcp login` to authenticate for richer config.\n'));
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
tier = auth.tier;
|
|
218
|
+
spinner.succeed(chalk.green(`Authenticated as ${auth.email} (${tier})`));
|
|
219
|
+
}
|
|
220
|
+
if (config) {
|
|
221
|
+
console.log(chalk.dim(` Product: ${config.product_name ?? config.product_id}`));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
console.log(chalk.yellow(' No .cutline/config.json found ā generating generic rules.'));
|
|
225
|
+
}
|
|
226
|
+
// Generate API key for authenticated users (skips if one already exists)
|
|
227
|
+
let generatedApiKey = null;
|
|
228
|
+
if (auth?.idToken && auth.baseUrl) {
|
|
229
|
+
const existing = loadConfig();
|
|
230
|
+
if (existing.apiKey) {
|
|
231
|
+
console.log(chalk.dim(' API key: already configured'));
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const keySpinner = ora('Generating API key...').start();
|
|
235
|
+
try {
|
|
236
|
+
const keyRes = await fetch(`${auth.baseUrl}/mcpDataProxy`, {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
headers: {
|
|
239
|
+
'Content-Type': 'application/json',
|
|
240
|
+
Authorization: `Bearer ${auth.idToken}`,
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify({ action: 'apiKey.create', params: { name: 'cursor-ide' } }),
|
|
243
|
+
});
|
|
244
|
+
if (keyRes.ok) {
|
|
245
|
+
const keyData = await keyRes.json();
|
|
246
|
+
generatedApiKey = keyData.key;
|
|
247
|
+
saveConfig({ apiKey: generatedApiKey });
|
|
248
|
+
keySpinner.succeed(chalk.green('API key generated and saved'));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const errBody = await keyRes.text().catch(() => '');
|
|
252
|
+
keySpinner.warn(chalk.yellow(`API key generation skipped (${keyRes.status})`));
|
|
253
|
+
if (errBody)
|
|
254
|
+
console.log(chalk.dim(` ${errBody.slice(0, 120)}`));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
keySpinner.warn(chalk.yellow('API key generation skipped (network error)'));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
console.log();
|
|
263
|
+
const filesWritten = [];
|
|
264
|
+
// 1. Cursor rules
|
|
265
|
+
const cursorDir = join(projectRoot, '.cursor', 'rules');
|
|
266
|
+
mkdirSync(cursorDir, { recursive: true });
|
|
267
|
+
writeFileSync(join(cursorDir, 'rgr-workflow.mdc'), cursorRgrRule(config, tier));
|
|
268
|
+
filesWritten.push('.cursor/rules/rgr-workflow.mdc');
|
|
269
|
+
writeFileSync(join(cursorDir, 'ambient-constraints.mdc'), cursorConstraintsRule(config, tier));
|
|
270
|
+
filesWritten.push('.cursor/rules/ambient-constraints.mdc');
|
|
271
|
+
if (tier === 'premium') {
|
|
272
|
+
writeFileSync(join(cursorDir, 'cutline.mdc'), cursorCutlinePointer());
|
|
273
|
+
filesWritten.push('.cursor/rules/cutline.mdc');
|
|
274
|
+
}
|
|
275
|
+
// 2. Claude Code config
|
|
276
|
+
writeFileSync(join(projectRoot, 'CLAUDE.local.md'), claudeLocalContent(config, tier));
|
|
277
|
+
filesWritten.push('CLAUDE.local.md');
|
|
278
|
+
// 3. Update .gitignore
|
|
279
|
+
const gitPatterns = ['CLAUDE.local.md', '.cursor/rules/', '.cutline.md'];
|
|
280
|
+
const gitignoreUpdated = ensureGitignore(projectRoot, gitPatterns);
|
|
281
|
+
for (const f of filesWritten) {
|
|
282
|
+
console.log(chalk.green(` ā ${f}`));
|
|
283
|
+
}
|
|
284
|
+
if (gitignoreUpdated) {
|
|
285
|
+
console.log(chalk.dim(' Updated .gitignore'));
|
|
286
|
+
}
|
|
287
|
+
console.log(chalk.bold(`\n ${filesWritten.length} files generated.`));
|
|
288
|
+
if (tier === 'premium' && config?.product_id) {
|
|
289
|
+
console.log(chalk.dim('\n For graph-enhanced .cutline.md, ask your AI agent:'));
|
|
290
|
+
console.log(chalk.cyan(` generate_cutline_md(product_id: "${config.product_id}", project_root: "${projectRoot}")`));
|
|
291
|
+
}
|
|
292
|
+
else if (tier === 'free') {
|
|
293
|
+
console.log(chalk.dim('\n Upgrade to Premium for product-specific constraint graphs and .cutline.md'));
|
|
294
|
+
console.log(chalk.dim(' ā'), chalk.cyan('cutline-mcp upgrade'), chalk.dim('or https://thecutline.ai/upgrade'));
|
|
295
|
+
}
|
|
296
|
+
if (generatedApiKey) {
|
|
297
|
+
console.log();
|
|
298
|
+
console.log(chalk.bold(' API Key (shown once):'));
|
|
299
|
+
console.log(chalk.cyan(` ${generatedApiKey}`));
|
|
300
|
+
console.log();
|
|
301
|
+
console.log(chalk.dim(' Add to your Cursor MCP config for best performance:'));
|
|
302
|
+
console.log(chalk.dim(' "env": { "CUTLINE_API_KEY": "') + chalk.cyan(generatedApiKey) + chalk.dim('" }'));
|
|
303
|
+
console.log();
|
|
304
|
+
console.log(chalk.dim(' Or it will be used automatically from ~/.cutline-mcp/config.json'));
|
|
305
|
+
}
|
|
306
|
+
console.log();
|
|
307
|
+
console.log(chalk.bold(' Next step:'));
|
|
308
|
+
console.log(chalk.dim(' Run'), chalk.cyan('cutline-mcp setup'), chalk.dim('to get the MCP server config for your IDE.\n'));
|
|
309
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
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 getSubscriptionStatus(idToken, isStaging) {
|
|
9
|
+
try {
|
|
10
|
+
const baseUrl = isStaging
|
|
11
|
+
? 'https://us-central1-cutline-staging.cloudfunctions.net'
|
|
12
|
+
: 'https://us-central1-cutline-prod.cloudfunctions.net';
|
|
13
|
+
const response = await fetch(`${baseUrl}/mcpSubscriptionStatus`, {
|
|
14
|
+
method: 'GET',
|
|
15
|
+
headers: {
|
|
16
|
+
'Authorization': `Bearer ${idToken}`,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
return { status: 'unknown' };
|
|
21
|
+
}
|
|
22
|
+
return await response.json();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return { status: 'unknown' };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function exchangeRefreshForIdToken(refreshToken, apiKey) {
|
|
29
|
+
const response = await fetch(`https://securetoken.googleapis.com/v1/token?key=${apiKey}`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
grant_type: 'refresh_token',
|
|
34
|
+
refresh_token: refreshToken,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error('Failed to get ID token');
|
|
39
|
+
}
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
return data.id_token;
|
|
42
|
+
}
|
|
43
|
+
async function exchangeCustomToken(customToken, apiKey) {
|
|
44
|
+
const response = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
token: customToken,
|
|
49
|
+
returnSecureToken: true,
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const error = await response.text();
|
|
54
|
+
throw new Error(`Failed to exchange custom token: ${error}`);
|
|
55
|
+
}
|
|
56
|
+
const data = await response.json();
|
|
57
|
+
return {
|
|
58
|
+
refreshToken: data.refreshToken,
|
|
59
|
+
email: data.email,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export async function loginCommand(options) {
|
|
63
|
+
const config = getConfig(options);
|
|
64
|
+
if (options.signup) {
|
|
65
|
+
console.log(chalk.bold('\nš Cutline MCP - Create Account\n'));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.log(chalk.bold('\nš Cutline MCP Authentication\n'));
|
|
69
|
+
}
|
|
70
|
+
if (options.staging) {
|
|
71
|
+
console.log(chalk.yellow(' ā ļø Using STAGING environment\n'));
|
|
72
|
+
}
|
|
73
|
+
if (options.email) {
|
|
74
|
+
console.log(chalk.gray(` Requesting sign-in as: ${options.email}\n`));
|
|
75
|
+
}
|
|
76
|
+
const spinner = ora('Starting authentication flow...').start();
|
|
77
|
+
try {
|
|
78
|
+
// Fetch Firebase API key from web app endpoint
|
|
79
|
+
spinner.text = 'Fetching configuration...';
|
|
80
|
+
let firebaseApiKey;
|
|
81
|
+
try {
|
|
82
|
+
firebaseApiKey = await fetchFirebaseApiKey(options);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
spinner.fail(chalk.red('Failed to fetch Firebase configuration'));
|
|
86
|
+
if (error instanceof Error) {
|
|
87
|
+
console.error(chalk.red(` ${error.message}`));
|
|
88
|
+
}
|
|
89
|
+
console.error(chalk.gray('\n You can also set the FIREBASE_API_KEY environment variable manually.\n'));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
// Start callback server
|
|
93
|
+
spinner.text = 'Waiting for authentication...';
|
|
94
|
+
const serverPromise = startCallbackServer(options.source ?? 'login');
|
|
95
|
+
// Open browser ā default is the quick email-only flow
|
|
96
|
+
let authUrl = `${config.AUTH_URL}?callback=${encodeURIComponent(config.CALLBACK_URL)}`;
|
|
97
|
+
if (options.signup) {
|
|
98
|
+
authUrl += '&mode=signup';
|
|
99
|
+
}
|
|
100
|
+
if (options.email) {
|
|
101
|
+
authUrl += `&email=${encodeURIComponent(options.email)}`;
|
|
102
|
+
}
|
|
103
|
+
await open(authUrl);
|
|
104
|
+
spinner.text = options.signup
|
|
105
|
+
? 'Browser opened - please create your account'
|
|
106
|
+
: 'Browser opened - enter your email to get started (check email for sign-in link)';
|
|
107
|
+
// Wait for callback with custom token
|
|
108
|
+
const result = await serverPromise;
|
|
109
|
+
// Exchange custom token for refresh token
|
|
110
|
+
spinner.text = 'Exchanging token...';
|
|
111
|
+
const { refreshToken, email } = await exchangeCustomToken(result.token, firebaseApiKey);
|
|
112
|
+
// Store refresh token
|
|
113
|
+
try {
|
|
114
|
+
await storeRefreshToken(refreshToken);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.warn(chalk.yellow(' ā ļø Could not save to Keychain (skipping)'));
|
|
118
|
+
}
|
|
119
|
+
// Save to file config (cross-platform)
|
|
120
|
+
try {
|
|
121
|
+
saveConfig({
|
|
122
|
+
refreshToken,
|
|
123
|
+
environment: options.staging ? 'staging' : 'production',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.error(chalk.red(' ā Failed to save config file:'), error);
|
|
128
|
+
}
|
|
129
|
+
spinner.succeed(chalk.green('Successfully authenticated!'));
|
|
130
|
+
// Show environment indicator
|
|
131
|
+
const envLabel = options.staging ? chalk.yellow('STAGING') : chalk.green('PRODUCTION');
|
|
132
|
+
console.log(chalk.gray(` Environment: ${envLabel}`));
|
|
133
|
+
if (email || result.email) {
|
|
134
|
+
console.log(chalk.gray(` Logged in as: ${email || result.email}`));
|
|
135
|
+
}
|
|
136
|
+
// Check subscription status
|
|
137
|
+
try {
|
|
138
|
+
spinner.start('Checking subscription...');
|
|
139
|
+
const idToken = await exchangeRefreshForIdToken(refreshToken, firebaseApiKey);
|
|
140
|
+
const subscription = await getSubscriptionStatus(idToken, !!options.staging);
|
|
141
|
+
spinner.stop();
|
|
142
|
+
if (subscription.status === 'active' || subscription.status === 'trialing') {
|
|
143
|
+
const statusLabel = subscription.status === 'trialing' ? ' (trial)' : '';
|
|
144
|
+
console.log(chalk.gray(' Plan:'), chalk.green(`ā ${subscription.planName || 'Premium'}${statusLabel}`));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
console.log(chalk.gray(' Plan:'), chalk.white('Free'));
|
|
148
|
+
console.log(chalk.dim(' Upgrade at'), chalk.cyan('https://thecutline.ai/pricing'));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
spinner.stop();
|
|
153
|
+
// Silently skip subscription check on error
|
|
154
|
+
}
|
|
155
|
+
console.log();
|
|
156
|
+
console.log(chalk.bold(' Next step:'));
|
|
157
|
+
console.log(chalk.dim(' Run'), chalk.cyan('cutline-mcp init'), chalk.dim('in your project directory to generate IDE rules.\n'));
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
spinner.fail(chalk.red('Authentication failed'));
|
|
161
|
+
if (error instanceof Error) {
|
|
162
|
+
console.error(chalk.red(` ${error.message}\n`));
|
|
163
|
+
}
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function logoutCommand(): Promise<void>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { deleteRefreshToken } from '../auth/keychain.js';
|
|
4
|
+
export async function logoutCommand() {
|
|
5
|
+
console.log(chalk.bold('\nš Logging out of Cutline MCP\n'));
|
|
6
|
+
const spinner = ora('Removing stored credentials...').start();
|
|
7
|
+
try {
|
|
8
|
+
const deleted = await deleteRefreshToken();
|
|
9
|
+
if (deleted) {
|
|
10
|
+
spinner.succeed(chalk.green('Successfully logged out'));
|
|
11
|
+
console.log(chalk.gray(' Credentials removed from keychain\n'));
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
spinner.info(chalk.yellow('No credentials found'));
|
|
15
|
+
console.log(chalk.gray(' You were not logged in\n'));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
spinner.fail(chalk.red('Logout failed'));
|
|
20
|
+
if (error instanceof Error) {
|
|
21
|
+
console.error(chalk.red(` ${error.message}\n`));
|
|
22
|
+
}
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function serveCommand(serverName: string): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { resolve, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const SERVER_MAP = {
|
|
7
|
+
constraints: 'cutline-server.js',
|
|
8
|
+
premortem: 'premortem-server.js',
|
|
9
|
+
exploration: 'exploration-server.js',
|
|
10
|
+
tools: 'tools-server.js',
|
|
11
|
+
output: 'output-server.js',
|
|
12
|
+
integrations: 'integrations-server.js',
|
|
13
|
+
};
|
|
14
|
+
export function serveCommand(serverName) {
|
|
15
|
+
const fileName = SERVER_MAP[serverName];
|
|
16
|
+
if (!fileName) {
|
|
17
|
+
const valid = Object.keys(SERVER_MAP).join(', ');
|
|
18
|
+
console.error(`Unknown server: "${serverName}". Valid names: ${valid}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const serverPath = resolve(__dirname, '../servers', fileName);
|
|
22
|
+
if (!existsSync(serverPath)) {
|
|
23
|
+
console.error(`Server bundle not found at ${serverPath}`);
|
|
24
|
+
console.error('The package may not have been built correctly.');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
// Replace this process with the MCP server.
|
|
28
|
+
// MCP servers use stdio transport, so we need to keep stdin/stdout connected.
|
|
29
|
+
try {
|
|
30
|
+
execFileSync(process.execPath, [serverPath], {
|
|
31
|
+
stdio: 'inherit',
|
|
32
|
+
env: process.env,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
process.exit(err.status ?? 1);
|
|
37
|
+
}
|
|
38
|
+
}
|