@vibekiln/cutline-mcp-cli 0.4.0 → 0.4.2
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/auth/keychain.js +1 -2
- package/dist/commands/setup.js +54 -10
- package/dist/servers/cutline-server.js +34 -17
- package/dist/utils/config-store.js +7 -0
- package/package.json +1 -1
package/dist/auth/keychain.js
CHANGED
package/dist/commands/setup.js
CHANGED
|
@@ -67,12 +67,12 @@ async function fetchProducts(idToken, options) {
|
|
|
67
67
|
headers: { Authorization: `Bearer ${idToken}` },
|
|
68
68
|
});
|
|
69
69
|
if (!res.ok)
|
|
70
|
-
return [];
|
|
70
|
+
return { products: [], requestOk: false };
|
|
71
71
|
const data = await res.json();
|
|
72
|
-
return data.products ?? [];
|
|
72
|
+
return { products: data.products ?? [], requestOk: true };
|
|
73
73
|
}
|
|
74
74
|
catch {
|
|
75
|
-
return [];
|
|
75
|
+
return { products: [], requestOk: false };
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
function prompt(question) {
|
|
@@ -237,12 +237,51 @@ export async function setupCommand(options) {
|
|
|
237
237
|
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
238
238
|
const configPath = join(projectRoot, '.cutline', 'config.json');
|
|
239
239
|
const hasExistingConfig = existsSync(configPath);
|
|
240
|
-
let
|
|
241
|
-
|
|
240
|
+
let existingConfig = null;
|
|
241
|
+
let hasUsableExistingConfig = false;
|
|
242
|
+
let graphConnected = false;
|
|
243
|
+
if (hasExistingConfig) {
|
|
244
|
+
try {
|
|
245
|
+
existingConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
246
|
+
hasUsableExistingConfig = Boolean(existingConfig?.product_id);
|
|
247
|
+
graphConnected = hasUsableExistingConfig;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
existingConfig = null;
|
|
251
|
+
hasUsableExistingConfig = false;
|
|
252
|
+
graphConnected = false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Validate that an existing binding is visible to the current account.
|
|
256
|
+
// This prevents showing "connected" when the repo has a product_id from
|
|
257
|
+
// a different account.
|
|
258
|
+
if (hasUsableExistingConfig && idToken) {
|
|
259
|
+
const currentEmail = (email ?? '').trim().toLowerCase();
|
|
260
|
+
const boundEmail = String(existingConfig?.linked_email ?? '').trim().toLowerCase();
|
|
261
|
+
if (boundEmail && currentEmail && boundEmail !== currentEmail) {
|
|
262
|
+
graphConnected = false;
|
|
263
|
+
hasUsableExistingConfig = false;
|
|
264
|
+
console.log(chalk.yellow(` Existing product binding is for ${existingConfig?.linked_email}, not ${email}.`));
|
|
265
|
+
console.log(chalk.dim(' Will not use this graph for the current account.\n'));
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
const { products, requestOk } = await fetchProducts(idToken, { staging: options.staging });
|
|
269
|
+
if (requestOk) {
|
|
270
|
+
const canAccessBoundProduct = products.some((p) => p.id === existingConfig?.product_id);
|
|
271
|
+
if (!canAccessBoundProduct) {
|
|
272
|
+
graphConnected = false;
|
|
273
|
+
hasUsableExistingConfig = false;
|
|
274
|
+
console.log(chalk.yellow(` Existing product graph is not accessible from ${email}.`));
|
|
275
|
+
console.log(chalk.dim(' Re-linking is required for this account.\n'));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (tier === 'premium' && idToken && !hasUsableExistingConfig) {
|
|
242
281
|
const productSpinner = ora('Fetching your product graphs...').start();
|
|
243
|
-
const products = await fetchProducts(idToken, { staging: options.staging });
|
|
282
|
+
const { products, requestOk } = await fetchProducts(idToken, { staging: options.staging });
|
|
244
283
|
productSpinner.stop();
|
|
245
|
-
if (products.length > 0) {
|
|
284
|
+
if (requestOk && products.length > 0) {
|
|
246
285
|
console.log(chalk.bold(' Connect to a product graph\n'));
|
|
247
286
|
products.forEach((p, i) => {
|
|
248
287
|
const date = p.createdAt ? chalk.dim(` (${new Date(p.createdAt).toLocaleDateString()})`) : '';
|
|
@@ -260,6 +299,7 @@ export async function setupCommand(options) {
|
|
|
260
299
|
writeFileSync(configPath, JSON.stringify({
|
|
261
300
|
product_id: selected.id,
|
|
262
301
|
product_name: selected.name,
|
|
302
|
+
linked_email: email ?? null,
|
|
263
303
|
}, null, 2) + '\n');
|
|
264
304
|
console.log(chalk.green(`\n ✓ Connected to "${selected.name}"`));
|
|
265
305
|
console.log(chalk.dim(` ${configPath}\n`));
|
|
@@ -269,16 +309,20 @@ export async function setupCommand(options) {
|
|
|
269
309
|
console.log(chalk.dim('\n Skipped. Run `cutline-mcp setup` again to connect later.\n'));
|
|
270
310
|
}
|
|
271
311
|
}
|
|
272
|
-
else {
|
|
312
|
+
else if (requestOk) {
|
|
273
313
|
console.log(chalk.dim(' No completed product graphs found.'));
|
|
274
314
|
console.log(chalk.dim(' Ask your AI agent to "Run a deep dive on my product idea" first,'));
|
|
275
315
|
console.log(chalk.dim(' then re-run'), chalk.cyan('cutline-mcp setup'), chalk.dim('to connect it.'));
|
|
276
316
|
console.log();
|
|
277
317
|
}
|
|
318
|
+
else {
|
|
319
|
+
console.log(chalk.yellow(' Could not verify product graphs (network/auth issue).'));
|
|
320
|
+
console.log(chalk.dim(' Re-run'), chalk.cyan('cutline-mcp setup'), chalk.dim('after auth/network is healthy.\n'));
|
|
321
|
+
}
|
|
278
322
|
}
|
|
279
|
-
else if (
|
|
323
|
+
else if (hasUsableExistingConfig) {
|
|
280
324
|
try {
|
|
281
|
-
const existing = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
325
|
+
const existing = existingConfig ?? JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
282
326
|
console.log(chalk.green(` ✓ Connected to product graph:`), chalk.white(existing.product_name || existing.product_id));
|
|
283
327
|
console.log();
|
|
284
328
|
}
|
|
@@ -7076,6 +7076,12 @@ async function handleSecurityScan(genericGraphId, uid, args, deps) {
|
|
|
7076
7076
|
total: scaFindings.length,
|
|
7077
7077
|
critical: scaFindings.filter((f) => f.severity === "critical").length,
|
|
7078
7078
|
high: scaFindings.filter((f) => f.severity === "high").length,
|
|
7079
|
+
all: scaFindings.map((f) => ({
|
|
7080
|
+
id: f.id,
|
|
7081
|
+
package: `${f.package_name}@${f.package_version}`,
|
|
7082
|
+
severity: f.severity,
|
|
7083
|
+
fix: f.fixed_version ? `Upgrade to ${f.fixed_version}` : void 0
|
|
7084
|
+
})),
|
|
7079
7085
|
top: scaFindings.slice(0, 5).map((f) => ({
|
|
7080
7086
|
id: f.id,
|
|
7081
7087
|
package: `${f.package_name}@${f.package_version}`,
|
|
@@ -7093,7 +7099,7 @@ function deltaStr(current, previous) {
|
|
|
7093
7099
|
return " (no change)";
|
|
7094
7100
|
return diff > 0 ? ` (**+${diff}** since last scan)` : ` (**${diff}** since last scan)`;
|
|
7095
7101
|
}
|
|
7096
|
-
function formatAuditOutput(result, reportId, publicSiteUrl = "https://thecutline.ai", hiddenAuditDimensions = []) {
|
|
7102
|
+
function formatAuditOutput(result, reportId, publicSiteUrl = "https://thecutline.ai", hiddenAuditDimensions = [], fullReport = false) {
|
|
7097
7103
|
const m = result.metrics;
|
|
7098
7104
|
const p = result.previousMetrics;
|
|
7099
7105
|
const isRescan = !!p;
|
|
@@ -7169,11 +7175,12 @@ function formatAuditOutput(result, reportId, publicSiteUrl = "https://thecutline
|
|
|
7169
7175
|
if (securityVisible && result.scaFindings && result.scaFindings.total > 0) {
|
|
7170
7176
|
const sca = result.scaFindings;
|
|
7171
7177
|
lines.push(``, `## Dependency Vulnerabilities (SCA)`, ``, `**${sca.total} known vulnerabilities** found (${sca.critical} critical, ${sca.high} high)`, ``);
|
|
7172
|
-
|
|
7178
|
+
const scaEntries = fullReport && sca.all?.length ? sca.all : sca.top;
|
|
7179
|
+
for (const v of scaEntries) {
|
|
7173
7180
|
const fix = v.fix ? ` \u2192 ${v.fix}` : "";
|
|
7174
7181
|
lines.push(`- **[${v.severity.toUpperCase()}]** ${v.package}: ${v.id}${fix}`);
|
|
7175
7182
|
}
|
|
7176
|
-
if (sca.total > sca.top.length) {
|
|
7183
|
+
if (!fullReport && sca.total > sca.top.length) {
|
|
7177
7184
|
lines.push(`- ...and ${sca.total - sca.top.length} more`);
|
|
7178
7185
|
}
|
|
7179
7186
|
}
|
|
@@ -7181,24 +7188,32 @@ function formatAuditOutput(result, reportId, publicSiteUrl = "https://thecutline
|
|
|
7181
7188
|
const totalFindings = visibleFindings.length;
|
|
7182
7189
|
const criticalCount = visibleFindings.filter((g) => g.severity === "critical" || g.severity === "high").length;
|
|
7183
7190
|
if (totalFindings > 0) {
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
lines.push(``, `## ${remaining.length} More Finding${remaining.length > 1 ? "s" : ""} Detected`);
|
|
7189
|
-
const teaserLimit = Math.min(remaining.length, 5);
|
|
7190
|
-
for (let i = 0; i < teaserLimit; i++) {
|
|
7191
|
-
lines.push(`- [${remaining[i].severity.toUpperCase()}] ${remaining[i].title}`);
|
|
7191
|
+
if (fullReport) {
|
|
7192
|
+
lines.push(``, `## Findings`, ``);
|
|
7193
|
+
for (const finding of visibleFindings) {
|
|
7194
|
+
lines.push(`**[${finding.severity.toUpperCase()}] ${finding.title}**`, `*Category: ${finding.category}*`, finding.description || "Address this finding to improve your readiness scores.", ``);
|
|
7192
7195
|
}
|
|
7193
|
-
|
|
7194
|
-
|
|
7196
|
+
lines.push(`> Re-run \`code_audit\` after fixes to measure score improvements.`);
|
|
7197
|
+
} else {
|
|
7198
|
+
const topFinding = visibleFindings[0];
|
|
7199
|
+
lines.push(``, `## #1 Finding \u2014 Fix This Now`, ``, `**[${topFinding.severity.toUpperCase()}] ${topFinding.title}**`, `*Category: ${topFinding.category}*`, ``, topFinding.description || "Address this finding to improve your readiness scores.", ``, `> Fix this issue, then re-run \`code_audit\` to see your scores improve.`);
|
|
7200
|
+
const remaining = visibleFindings.slice(1);
|
|
7201
|
+
if (remaining.length > 0) {
|
|
7202
|
+
lines.push(``, `## ${remaining.length} More Finding${remaining.length > 1 ? "s" : ""} Detected`);
|
|
7203
|
+
const teaserLimit = Math.min(remaining.length, 5);
|
|
7204
|
+
for (let i = 0; i < teaserLimit; i++) {
|
|
7205
|
+
lines.push(`- [${remaining[i].severity.toUpperCase()}] ${remaining[i].title}`);
|
|
7206
|
+
}
|
|
7207
|
+
if (remaining.length > teaserLimit) {
|
|
7208
|
+
lines.push(`- ... and **${remaining.length - teaserLimit} more**`);
|
|
7209
|
+
}
|
|
7195
7210
|
}
|
|
7196
7211
|
}
|
|
7197
7212
|
}
|
|
7198
7213
|
lines.push(``, `---`, ``);
|
|
7199
|
-
if (totalFindings > 1) {
|
|
7214
|
+
if (!fullReport && totalFindings > 1) {
|
|
7200
7215
|
lines.push(`### Unlock Full Analysis`, ``, `You fixed one \u2014 **${totalFindings - 1} findings** remain (${criticalCount} critical/high).`, `Upgrade to Cutline Pro for:`, `- Full details and remediation for every finding`, `- Prioritized RGR remediation plans your coding agent can execute`, `- Product-specific deep dive with feature-level constraint mapping`, `- Continuous score tracking with this audit as a baseline prior`, ``, `\u2192 Run \`cutline-mcp upgrade\` or visit **https://thecutline.ai/upgrade**`);
|
|
7201
|
-
} else if (totalFindings
|
|
7216
|
+
} else if (totalFindings >= 1) {
|
|
7202
7217
|
lines.push(`### Next Steps`, ``, `Fix the finding above, then re-scan to see your scores improve.`, `When ready, run a **deep dive** for product-specific analysis \u2014`, `these generic scores will serve as a prior for your product graph.`);
|
|
7203
7218
|
} else {
|
|
7204
7219
|
lines.push(`### Next Steps`, ``, `No critical findings detected. Run a **deep dive** for product-specific`, `analysis with feature coverage and the generic scores as a prior.`);
|
|
@@ -8423,15 +8438,17 @@ Why AI: ${idea.whyAI}`
|
|
|
8423
8438
|
return "engineering";
|
|
8424
8439
|
return "security";
|
|
8425
8440
|
};
|
|
8441
|
+
let hasPremiumSubscription = false;
|
|
8426
8442
|
{
|
|
8427
8443
|
const rateInfo = await getScanRateLimit();
|
|
8444
|
+
hasPremiumSubscription = rateInfo.subscription === "active" || rateInfo.subscription === "trialing";
|
|
8428
8445
|
const now = /* @__PURE__ */ new Date();
|
|
8429
8446
|
const resetAt = rateInfo.periodStart ? new Date(rateInfo.periodStart) : /* @__PURE__ */ new Date(0);
|
|
8430
8447
|
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
8431
8448
|
if (resetAt < monthStart) {
|
|
8432
8449
|
await updateScanRateLimit({ free_scan_counter: { count: 1, reset_at: now.toISOString() } });
|
|
8433
8450
|
} else if (rateInfo.scanCount >= 3) {
|
|
8434
|
-
if (
|
|
8451
|
+
if (!hasPremiumSubscription) {
|
|
8435
8452
|
throw new McpError(ErrorCode.InvalidRequest, "Free scan limit reached (3/month). Run `cutline-mcp upgrade` in your terminal, or visit https://thecutline.ai/upgrade");
|
|
8436
8453
|
}
|
|
8437
8454
|
} else {
|
|
@@ -8500,7 +8517,7 @@ Why AI: ${idea.whyAI}`
|
|
|
8500
8517
|
console.error("[code_audit] Report persistence failed (non-fatal):", e);
|
|
8501
8518
|
}
|
|
8502
8519
|
return {
|
|
8503
|
-
content: [{ type: "text", text: formatAuditOutput(result, reportId, reportSiteUrl, hiddenAuditDimensions) }]
|
|
8520
|
+
content: [{ type: "text", text: formatAuditOutput(result, reportId, reportSiteUrl, hiddenAuditDimensions, hasPremiumSubscription) }]
|
|
8504
8521
|
};
|
|
8505
8522
|
}
|
|
8506
8523
|
const authCtx = await resolveAuthContext(args.auth_token);
|
|
@@ -12,6 +12,13 @@ export function saveConfig(config) {
|
|
|
12
12
|
ensureConfigDir();
|
|
13
13
|
const current = loadConfig();
|
|
14
14
|
const newConfig = { ...current, ...config };
|
|
15
|
+
// Treat explicit undefined values as key deletion so callers can clear
|
|
16
|
+
// persisted fields (for example during logout).
|
|
17
|
+
for (const [key, value] of Object.entries(config)) {
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
delete newConfig[key];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
15
22
|
// Remove legacy fields that are no longer stored (e.g. firebaseApiKey)
|
|
16
23
|
// to prevent stale values from causing cross-project auth mismatches
|
|
17
24
|
delete newConfig.firebaseApiKey;
|
package/package.json
CHANGED