@vibekiln/cutline-mcp-cli 0.4.1 → 0.4.3

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.
@@ -204,6 +204,9 @@ function ensureGitignore(projectRoot, patterns) {
204
204
  export async function initCommand(options) {
205
205
  const projectRoot = resolve(options.projectRoot ?? process.cwd());
206
206
  const config = readCutlineConfig(projectRoot);
207
+ const scanUrl = options.staging
208
+ ? 'https://cutline-staging.web.app/scan'
209
+ : 'https://thecutline.ai/scan';
207
210
  console.log(chalk.bold('\n🔧 Cutline Init\n'));
208
211
  // Authenticate and determine tier
209
212
  const spinner = ora('Checking authentication...').start();
@@ -212,6 +215,8 @@ export async function initCommand(options) {
212
215
  if (!auth) {
213
216
  spinner.warn(chalk.yellow('Not authenticated — generating free-tier rules'));
214
217
  console.log(chalk.dim(' Run `cutline-mcp login` to authenticate for richer config.\n'));
218
+ console.log(chalk.dim(' Or run a quick scan in the web app:'), chalk.cyan(scanUrl));
219
+ console.log();
215
220
  }
216
221
  else {
217
222
  tier = auth.tier;
@@ -292,6 +297,7 @@ export async function initCommand(options) {
292
297
  else if (tier === 'free') {
293
298
  console.log(chalk.dim('\n Upgrade to Premium for product-specific constraint graphs and .cutline.md'));
294
299
  console.log(chalk.dim(' →'), chalk.cyan('cutline-mcp upgrade'), chalk.dim('or https://thecutline.ai/upgrade'));
300
+ console.log(chalk.dim(' Free scan in browser:'), chalk.cyan(scanUrl));
295
301
  }
296
302
  if (generatedApiKey) {
297
303
  console.log();
@@ -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 graphConnected = hasExistingConfig;
241
- if (tier === 'premium' && idToken && !hasExistingConfig) {
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 (hasExistingConfig) {
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
  }
@@ -305,13 +305,17 @@ function getHiddenAuditDimensions() {
305
305
  const normalized = [...new Set(hidden.map((d) => String(d).trim().toLowerCase()).filter((d) => allowed.has(d)))];
306
306
  return normalized;
307
307
  }
308
- function getStoredApiKey() {
308
+ function getStoredApiKey(options) {
309
+ const includeConfig = options?.includeConfig ?? true;
309
310
  if (process.env.CUTLINE_API_KEY) {
310
311
  return {
311
312
  apiKey: process.env.CUTLINE_API_KEY,
312
313
  environment: process.env.CUTLINE_ENV === "staging" ? "staging" : "production"
313
314
  };
314
315
  }
316
+ if (!includeConfig) {
317
+ return null;
318
+ }
315
319
  const config = readLocalCutlineConfig();
316
320
  if (config?.apiKey) {
317
321
  return { apiKey: config.apiKey, environment: config.environment };
@@ -406,12 +410,15 @@ function getSiteUrl(environment) {
406
410
  return environment === "staging" ? "https://cutline-staging.web.app" : "https://thecutline.ai";
407
411
  }
408
412
  async function getPublicSiteUrlForCurrentAuth() {
413
+ const stored = await getStoredToken();
414
+ if (stored?.environment) {
415
+ return getSiteUrl(stored.environment);
416
+ }
409
417
  const apiKeyInfo = getStoredApiKey();
410
418
  if (apiKeyInfo?.environment) {
411
419
  return getSiteUrl(apiKeyInfo.environment);
412
420
  }
413
- const stored = await getStoredToken();
414
- return getSiteUrl(stored?.environment);
421
+ return getSiteUrl();
415
422
  }
416
423
  var cachedBaseUrl;
417
424
  var cachedIdToken;
@@ -455,27 +462,34 @@ async function exchangeRefreshForId(refreshToken, environment) {
455
462
  return data.id_token;
456
463
  }
457
464
  async function resolveAuth() {
458
- const apiKeyInfo = getStoredApiKey();
459
- if (apiKeyInfo) {
465
+ const envApiKeyInfo = getStoredApiKey({ includeConfig: false });
466
+ if (envApiKeyInfo) {
460
467
  return {
461
- baseUrl: getBaseUrl(apiKeyInfo.environment),
462
- idToken: apiKeyInfo.apiKey
468
+ baseUrl: getBaseUrl(envApiKeyInfo.environment),
469
+ idToken: envApiKeyInfo.apiKey
463
470
  };
464
471
  }
465
472
  if (cachedIdToken && Date.now() < tokenExpiresAt && cachedBaseUrl) {
466
473
  return { baseUrl: cachedBaseUrl, idToken: cachedIdToken };
467
474
  }
468
475
  const stored = await getStoredToken();
469
- if (!stored) {
470
- throw new Error("Not authenticated. Run 'cutline-mcp login' or set CUTLINE_API_KEY.");
476
+ if (stored) {
477
+ const env = stored.environment || "production";
478
+ const baseUrl = getBaseUrl(env);
479
+ const idToken = await exchangeRefreshForId(stored.refreshToken, env);
480
+ cachedBaseUrl = baseUrl;
481
+ cachedIdToken = idToken;
482
+ tokenExpiresAt = Date.now() + 50 * 60 * 1e3;
483
+ return { baseUrl, idToken };
484
+ }
485
+ const configApiKeyInfo = getStoredApiKey();
486
+ if (configApiKeyInfo) {
487
+ return {
488
+ baseUrl: getBaseUrl(configApiKeyInfo.environment),
489
+ idToken: configApiKeyInfo.apiKey
490
+ };
471
491
  }
472
- const env = stored.environment || "production";
473
- const baseUrl = getBaseUrl(env);
474
- const idToken = await exchangeRefreshForId(stored.refreshToken, env);
475
- cachedBaseUrl = baseUrl;
476
- cachedIdToken = idToken;
477
- tokenExpiresAt = Date.now() + 50 * 60 * 1e3;
478
- return { baseUrl, idToken };
492
+ throw new Error("Not authenticated. Run 'cutline-mcp login' or set CUTLINE_API_KEY.");
479
493
  }
480
494
  async function proxy(action, params = {}) {
481
495
  const { baseUrl, idToken } = await resolveAuth();
@@ -75,7 +75,7 @@ import {
75
75
  upsertEntities,
76
76
  upsertNodes,
77
77
  validateRequestSize
78
- } from "./chunk-6Y3AEXE3.js";
78
+ } from "./chunk-X2B5QUWO.js";
79
79
  import {
80
80
  GraphTraverser,
81
81
  computeGenericGraphMetrics,
@@ -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}`,
@@ -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
- for (const v of sca.top) {
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
  }
@@ -78,7 +78,7 @@ import {
78
78
  upsertEdges,
79
79
  upsertEntities,
80
80
  upsertNodes
81
- } from "./chunk-6Y3AEXE3.js";
81
+ } from "./chunk-X2B5QUWO.js";
82
82
  export {
83
83
  addEdges,
84
84
  addEntity,
@@ -14,7 +14,7 @@ import {
14
14
  requirePremiumWithAutoAuth,
15
15
  updateExplorationSession,
16
16
  validateRequestSize
17
- } from "./chunk-6Y3AEXE3.js";
17
+ } from "./chunk-X2B5QUWO.js";
18
18
 
19
19
  // ../mcp/dist/mcp/src/exploration-server.js
20
20
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -13,7 +13,7 @@ import {
13
13
  requirePremiumWithAutoAuth,
14
14
  validateAuth,
15
15
  validateRequestSize
16
- } from "./chunk-6Y3AEXE3.js";
16
+ } from "./chunk-X2B5QUWO.js";
17
17
 
18
18
  // ../mcp/dist/mcp/src/integrations-server.js
19
19
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -13,7 +13,7 @@ import {
13
13
  mapErrorToMcp,
14
14
  requirePremiumWithAutoAuth,
15
15
  validateRequestSize
16
- } from "./chunk-6Y3AEXE3.js";
16
+ } from "./chunk-X2B5QUWO.js";
17
17
 
18
18
  // ../mcp/dist/mcp/src/output-server.js
19
19
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -27,7 +27,7 @@ import {
27
27
  updatePremortem,
28
28
  validateAuth,
29
29
  validateRequestSize
30
- } from "./chunk-6Y3AEXE3.js";
30
+ } from "./chunk-X2B5QUWO.js";
31
31
 
32
32
  // ../mcp/dist/mcp/src/premortem-server.js
33
33
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -21,7 +21,7 @@ import {
21
21
  requirePremiumWithAutoAuth,
22
22
  validateAuth,
23
23
  validateRequestSize
24
- } from "./chunk-6Y3AEXE3.js";
24
+ } from "./chunk-X2B5QUWO.js";
25
25
 
26
26
  // ../mcp/dist/mcp/src/tools-server.js
27
27
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibekiln/cutline-mcp-cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "CLI and MCP servers for Cutline — authenticate, then run constraint-aware MCP servers in Cursor or any MCP client.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",