@leanmcp/cli 0.5.6 → 0.5.8

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.
Files changed (3) hide show
  1. package/README.md +477 -477
  2. package/dist/index.js +428 -118
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -3,9 +3,9 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
3
3
 
4
4
  // src/index.ts
5
5
  import { Command } from "commander";
6
- import fs11 from "fs-extra";
7
- import path11 from "path";
8
- import ora8 from "ora";
6
+ import fs13 from "fs-extra";
7
+ import path13 from "path";
8
+ import ora9 from "ora";
9
9
  import { createRequire } from "module";
10
10
  import { confirm as confirm4 } from "@inquirer/prompts";
11
11
  import { spawn as spawn4 } from "child_process";
@@ -13,8 +13,8 @@ import { spawn as spawn4 } from "child_process";
13
13
  // src/commands/dev.ts
14
14
  import { spawn } from "child_process";
15
15
  import ora from "ora";
16
- import path3 from "path";
17
- import fs3 from "fs-extra";
16
+ import path4 from "path";
17
+ import fs4 from "fs-extra";
18
18
  import crypto2 from "crypto";
19
19
  import chokidar from "chokidar";
20
20
 
@@ -471,6 +471,8 @@ __name(deleteUIComponent, "deleteUIComponent");
471
471
  import chalk from "chalk";
472
472
  import os from "os";
473
473
  import crypto from "crypto";
474
+ import path3 from "path";
475
+ import fs3 from "fs-extra";
474
476
  import { execSync } from "child_process";
475
477
  var POSTHOG_API_KEY = "phc_EoMHKFbx6j2wUFsf8ywqgHntY4vEXC3ZzLFoPJVjRRT";
476
478
  var POSTHOG_API_HOST = "https://d18m0xvdtnkibr.cloudfront.net";
@@ -568,12 +570,40 @@ var sendToPostHog = /* @__PURE__ */ __name((eventName, properties = {}) => {
568
570
  }
569
571
  });
570
572
  }, "sendToPostHog");
573
+ var LOG_DIR = path3.join(os.homedir(), ".leanmcp", "logs");
574
+ try {
575
+ fs3.ensureDirSync(LOG_DIR);
576
+ } catch (err) {
577
+ }
578
+ function getTimestamp() {
579
+ return (/* @__PURE__ */ new Date()).toISOString();
580
+ }
581
+ __name(getTimestamp, "getTimestamp");
582
+ function getLogFilePath() {
583
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
584
+ return path3.join(LOG_DIR, `cli-${date}.log`);
585
+ }
586
+ __name(getLogFilePath, "getLogFilePath");
587
+ function redactSensitive(text) {
588
+ if (!text) return text;
589
+ return text.replace(/(leanmcp_|airtain_)[a-zA-Z0-9]{20,}/g, "$1********************");
590
+ }
591
+ __name(redactSensitive, "redactSensitive");
571
592
  var LoggerClass = class LoggerClass2 {
572
593
  static {
573
594
  __name(this, "LoggerClass");
574
595
  }
596
+ writeToFile(level, message) {
597
+ try {
598
+ const timestamp = getTimestamp();
599
+ const logLine = `[${timestamp}] [${level.toUpperCase()}] ${redactSensitive(message)}
600
+ `;
601
+ fs3.appendFileSync(getLogFilePath(), logLine);
602
+ } catch (err) {
603
+ }
604
+ }
575
605
  /**
576
- * Log a message to console and send cli_log event to PostHog
606
+ * Log a message to console, file, and send cli_log event to PostHog
577
607
  * @param text - The text to log
578
608
  * @param styleFn - Optional chalk style function (e.g., chalk.cyan, chalk.green)
579
609
  */
@@ -583,32 +613,35 @@ var LoggerClass = class LoggerClass2 {
583
613
  } else {
584
614
  console.log(text);
585
615
  }
616
+ this.writeToFile("log", text);
586
617
  sendToPostHog("cli_log", {
587
618
  message: text,
588
619
  level: "log"
589
620
  });
590
621
  }
591
622
  /**
592
- * Log a warning to console and send cli_warn event to PostHog
623
+ * Log a warning to console, file, and send cli_warn event to PostHog
593
624
  * @param text - The warning text
594
625
  * @param styleFn - Optional chalk style function (defaults to chalk.yellow)
595
626
  */
596
627
  warn(text, styleFn) {
597
628
  const style = styleFn || chalk.yellow;
598
629
  console.log(style(text));
630
+ this.writeToFile("warn", text);
599
631
  sendToPostHog("cli_warn", {
600
632
  message: text,
601
633
  level: "warn"
602
634
  });
603
635
  }
604
636
  /**
605
- * Log an error to console and send cli_error event to PostHog
637
+ * Log an error to console, file, and send cli_error event to PostHog
606
638
  * @param text - The error text
607
639
  * @param styleFn - Optional chalk style function (defaults to chalk.red)
608
640
  */
609
641
  error(text, styleFn) {
610
642
  const style = styleFn || chalk.red;
611
643
  console.error(style(text));
644
+ this.writeToFile("error", text);
612
645
  sendToPostHog("cli_error", {
613
646
  message: text,
614
647
  level: "error"
@@ -620,6 +653,7 @@ var LoggerClass = class LoggerClass2 {
620
653
  */
621
654
  info(text) {
622
655
  console.log(chalk.cyan(text));
656
+ this.writeToFile("info", text);
623
657
  sendToPostHog("cli_log", {
624
658
  message: text,
625
659
  level: "info"
@@ -631,6 +665,7 @@ var LoggerClass = class LoggerClass2 {
631
665
  */
632
666
  success(text) {
633
667
  console.log(chalk.green(text));
668
+ this.writeToFile("success", text);
634
669
  sendToPostHog("cli_log", {
635
670
  message: text,
636
671
  level: "success"
@@ -642,6 +677,7 @@ var LoggerClass = class LoggerClass2 {
642
677
  */
643
678
  gray(text) {
644
679
  console.log(chalk.gray(text));
680
+ this.writeToFile("debug", text);
645
681
  sendToPostHog("cli_log", {
646
682
  message: text,
647
683
  level: "gray"
@@ -658,7 +694,7 @@ var trackCommand = /* @__PURE__ */ __name((command, options = {}) => {
658
694
 
659
695
  // src/commands/dev.ts
660
696
  function computeHash(filePath) {
661
- const content = fs3.readFileSync(filePath, "utf-8");
697
+ const content = fs4.readFileSync(filePath, "utf-8");
662
698
  return crypto2.createHash("md5").update(content).digest("hex");
663
699
  }
664
700
  __name(computeHash, "computeHash");
@@ -675,7 +711,7 @@ function computeDiff(previousUIApps, currentUIApps, hashCache) {
675
711
  } else {
676
712
  const oldHash = hashCache.get(app.resourceUri);
677
713
  let newHash;
678
- if (fs3.existsSync(app.componentPath)) {
714
+ if (fs4.existsSync(app.componentPath)) {
679
715
  newHash = computeHash(app.componentPath);
680
716
  }
681
717
  if (oldHash !== newHash) {
@@ -692,7 +728,7 @@ function computeDiff(previousUIApps, currentUIApps, hashCache) {
692
728
  __name(computeDiff, "computeDiff");
693
729
  async function devCommand() {
694
730
  const cwd = process.cwd();
695
- if (!await fs3.pathExists(path3.join(cwd, "main.ts"))) {
731
+ if (!await fs4.pathExists(path4.join(cwd, "main.ts"))) {
696
732
  logger.error("ERROR: Not a LeanMCP project (main.ts not found).");
697
733
  logger.gray("Run this command from your project root.");
698
734
  process.exit(1);
@@ -748,12 +784,12 @@ async function devCommand() {
748
784
  let watcher = null;
749
785
  const componentHashCache = /* @__PURE__ */ new Map();
750
786
  for (const app of uiApps) {
751
- if (await fs3.pathExists(app.componentPath)) {
787
+ if (await fs4.pathExists(app.componentPath)) {
752
788
  componentHashCache.set(app.resourceUri, computeHash(app.componentPath));
753
789
  }
754
790
  }
755
791
  let previousUIApps = uiApps;
756
- const mcpPath = path3.join(cwd, "mcp");
792
+ const mcpPath = path4.join(cwd, "mcp");
757
793
  watcher = chokidar.watch(mcpPath, {
758
794
  ignoreInitial: true,
759
795
  ignored: [
@@ -795,7 +831,7 @@ async function devCommand() {
795
831
  } else {
796
832
  manifest[app.resourceUri] = result.htmlPath;
797
833
  }
798
- if (await fs3.pathExists(app.componentPath)) {
834
+ if (await fs4.pathExists(app.componentPath)) {
799
835
  componentHashCache.set(app.resourceUri, computeHash(app.componentPath));
800
836
  }
801
837
  logger.success(`${app.componentName} ${action.toLowerCase()} complete`);
@@ -841,12 +877,12 @@ __name(devCommand, "devCommand");
841
877
  // src/commands/build.ts
842
878
  import { spawn as spawn2 } from "child_process";
843
879
  import ora2 from "ora";
844
- import path5 from "path";
845
- import fs5 from "fs-extra";
880
+ import path6 from "path";
881
+ import fs6 from "fs-extra";
846
882
 
847
883
  // src/schema-extractor.ts
848
- import path4 from "path";
849
- import fs4 from "fs-extra";
884
+ import path5 from "path";
885
+ import fs5 from "fs-extra";
850
886
  import { Project, Node } from "ts-morph";
851
887
  async function generateSchemaMetadata(projectDir) {
852
888
  const metadata = {};
@@ -921,9 +957,9 @@ async function generateSchemaMetadata(projectDir) {
921
957
  }
922
958
  processedClasses.add(className);
923
959
  }
924
- const outputPath = path4.join(projectDir, "dist", "schema-metadata.json");
925
- await fs4.ensureDir(path4.dirname(outputPath));
926
- await fs4.writeJSON(outputPath, metadata, {
960
+ const outputPath = path5.join(projectDir, "dist", "schema-metadata.json");
961
+ await fs5.ensureDir(path5.dirname(outputPath));
962
+ await fs5.writeJSON(outputPath, metadata, {
927
963
  spaces: 2
928
964
  });
929
965
  }
@@ -958,11 +994,11 @@ __name(extractBaseClassName, "extractBaseClassName");
958
994
  async function findTsFiles(dir) {
959
995
  const files = [];
960
996
  async function scan(currentDir) {
961
- const entries = await fs4.readdir(currentDir, {
997
+ const entries = await fs5.readdir(currentDir, {
962
998
  withFileTypes: true
963
999
  });
964
1000
  for (const entry of entries) {
965
- const fullPath = path4.join(currentDir, entry.name);
1001
+ const fullPath = path5.join(currentDir, entry.name);
966
1002
  if (entry.isDirectory()) {
967
1003
  if ([
968
1004
  "node_modules",
@@ -1028,7 +1064,7 @@ __name(mapPrimitiveType, "mapPrimitiveType");
1028
1064
  // src/commands/build.ts
1029
1065
  async function buildCommand() {
1030
1066
  const cwd = process.cwd();
1031
- if (!await fs5.pathExists(path5.join(cwd, "main.ts"))) {
1067
+ if (!await fs6.pathExists(path6.join(cwd, "main.ts"))) {
1032
1068
  logger.error("ERROR: Not a LeanMCP project (main.ts not found).");
1033
1069
  logger.gray("Run this command from your project root.");
1034
1070
  process.exit(1);
@@ -1110,11 +1146,11 @@ __name(buildCommand, "buildCommand");
1110
1146
  // src/commands/start.ts
1111
1147
  import { spawn as spawn3 } from "child_process";
1112
1148
  import ora3 from "ora";
1113
- import path6 from "path";
1114
- import fs6 from "fs-extra";
1149
+ import path7 from "path";
1150
+ import fs7 from "fs-extra";
1115
1151
  async function startCommand() {
1116
1152
  const cwd = process.cwd();
1117
- if (!await fs6.pathExists(path6.join(cwd, "main.ts"))) {
1153
+ if (!await fs7.pathExists(path7.join(cwd, "main.ts"))) {
1118
1154
  logger.error("ERROR: Not a LeanMCP project (main.ts not found).");
1119
1155
  logger.gray("Run this command from your project root.");
1120
1156
  process.exit(1);
@@ -1216,8 +1252,8 @@ __name(startCommand, "startCommand");
1216
1252
 
1217
1253
  // src/commands/login.ts
1218
1254
  import ora4 from "ora";
1219
- import path7 from "path";
1220
- import fs7 from "fs-extra";
1255
+ import path8 from "path";
1256
+ import fs8 from "fs-extra";
1221
1257
  import os2 from "os";
1222
1258
  import { input, confirm } from "@inquirer/prompts";
1223
1259
  var DEBUG_MODE2 = false;
@@ -1231,12 +1267,12 @@ function debug2(message, ...args) {
1231
1267
  }
1232
1268
  }
1233
1269
  __name(debug2, "debug");
1234
- var CONFIG_DIR = path7.join(os2.homedir(), ".leanmcp");
1235
- var CONFIG_FILE = path7.join(CONFIG_DIR, "config.json");
1270
+ var CONFIG_DIR = path8.join(os2.homedir(), ".leanmcp");
1271
+ var CONFIG_FILE = path8.join(CONFIG_DIR, "config.json");
1236
1272
  async function loadConfig() {
1237
1273
  try {
1238
- if (await fs7.pathExists(CONFIG_FILE)) {
1239
- return await fs7.readJSON(CONFIG_FILE);
1274
+ if (await fs8.pathExists(CONFIG_FILE)) {
1275
+ return await fs8.readJSON(CONFIG_FILE);
1240
1276
  }
1241
1277
  } catch (error) {
1242
1278
  }
@@ -1244,8 +1280,8 @@ async function loadConfig() {
1244
1280
  }
1245
1281
  __name(loadConfig, "loadConfig");
1246
1282
  async function saveConfig(config) {
1247
- await fs7.ensureDir(CONFIG_DIR);
1248
- await fs7.writeJSON(CONFIG_FILE, config, {
1283
+ await fs8.ensureDir(CONFIG_DIR);
1284
+ await fs8.writeJSON(CONFIG_FILE, config, {
1249
1285
  spaces: 2
1250
1286
  });
1251
1287
  }
@@ -1452,8 +1488,8 @@ __name(whoamiCommand, "whoamiCommand");
1452
1488
 
1453
1489
  // src/commands/deploy.ts
1454
1490
  import ora5 from "ora";
1455
- import path9 from "path";
1456
- import fs9 from "fs-extra";
1491
+ import path10 from "path";
1492
+ import fs10 from "fs-extra";
1457
1493
  import os3 from "os";
1458
1494
  import archiver from "archiver";
1459
1495
  import { input as input2, confirm as confirm2, select } from "@inquirer/prompts";
@@ -1636,8 +1672,8 @@ function generateProjectName() {
1636
1672
  __name(generateProjectName, "generateProjectName");
1637
1673
 
1638
1674
  // src/utils/env-parser.ts
1639
- import fs8 from "fs-extra";
1640
- import path8 from "path";
1675
+ import fs9 from "fs-extra";
1676
+ import path9 from "path";
1641
1677
  var RESERVED_ENV_KEYS = [
1642
1678
  "AWS_REGION",
1643
1679
  "AWS_ACCESS_KEY_ID",
@@ -1700,16 +1736,16 @@ function parseEnvFile(content) {
1700
1736
  }
1701
1737
  __name(parseEnvFile, "parseEnvFile");
1702
1738
  async function loadEnvFile(filePath) {
1703
- const absolutePath = path8.resolve(filePath);
1704
- if (!await fs8.pathExists(absolutePath)) {
1739
+ const absolutePath = path9.resolve(filePath);
1740
+ if (!await fs9.pathExists(absolutePath)) {
1705
1741
  throw new Error(`Env file not found: ${absolutePath}`);
1706
1742
  }
1707
- const content = await fs8.readFile(absolutePath, "utf-8");
1743
+ const content = await fs9.readFile(absolutePath, "utf-8");
1708
1744
  return parseEnvFile(content);
1709
1745
  }
1710
1746
  __name(loadEnvFile, "loadEnvFile");
1711
1747
  async function writeEnvFile(filePath, vars) {
1712
- const absolutePath = path8.resolve(filePath);
1748
+ const absolutePath = path9.resolve(filePath);
1713
1749
  const lines = [
1714
1750
  "# Environment variables",
1715
1751
  `# Generated by leanmcp CLI at ${(/* @__PURE__ */ new Date()).toISOString()}`,
@@ -1725,7 +1761,7 @@ async function writeEnvFile(filePath, vars) {
1725
1761
  }
1726
1762
  }
1727
1763
  lines.push("");
1728
- await fs8.writeFile(absolutePath, lines.join("\n"));
1764
+ await fs9.writeFile(absolutePath, lines.join("\n"));
1729
1765
  }
1730
1766
  __name(writeEnvFile, "writeEnvFile");
1731
1767
  function isValidEnvKey(key) {
@@ -1839,10 +1875,10 @@ var API_ENDPOINTS = {
1839
1875
  var LEANMCP_CONFIG_DIR = ".leanmcp";
1840
1876
  var LEANMCP_CONFIG_FILE = "config.json";
1841
1877
  async function readLeanMCPConfig(projectPath) {
1842
- const configPath = path9.join(projectPath, LEANMCP_CONFIG_DIR, LEANMCP_CONFIG_FILE);
1878
+ const configPath = path10.join(projectPath, LEANMCP_CONFIG_DIR, LEANMCP_CONFIG_FILE);
1843
1879
  try {
1844
- if (await fs9.pathExists(configPath)) {
1845
- const config = await fs9.readJSON(configPath);
1880
+ if (await fs10.pathExists(configPath)) {
1881
+ const config = await fs10.readJSON(configPath);
1846
1882
  debug3("Found existing .leanmcp config:", config);
1847
1883
  return config;
1848
1884
  }
@@ -1853,10 +1889,10 @@ async function readLeanMCPConfig(projectPath) {
1853
1889
  }
1854
1890
  __name(readLeanMCPConfig, "readLeanMCPConfig");
1855
1891
  async function writeLeanMCPConfig(projectPath, config) {
1856
- const configDir = path9.join(projectPath, LEANMCP_CONFIG_DIR);
1857
- const configPath = path9.join(configDir, LEANMCP_CONFIG_FILE);
1858
- await fs9.ensureDir(configDir);
1859
- await fs9.writeJSON(configPath, config, {
1892
+ const configDir = path10.join(projectPath, LEANMCP_CONFIG_DIR);
1893
+ const configPath = path10.join(configDir, LEANMCP_CONFIG_FILE);
1894
+ await fs10.ensureDir(configDir);
1895
+ await fs10.writeJSON(configPath, config, {
1860
1896
  spaces: 2
1861
1897
  });
1862
1898
  debug3("Saved .leanmcp config:", config);
@@ -1864,7 +1900,7 @@ async function writeLeanMCPConfig(projectPath, config) {
1864
1900
  __name(writeLeanMCPConfig, "writeLeanMCPConfig");
1865
1901
  async function createZipArchive(folderPath, outputPath) {
1866
1902
  return new Promise((resolve, reject) => {
1867
- const output = fs9.createWriteStream(outputPath);
1903
+ const output = fs10.createWriteStream(outputPath);
1868
1904
  const archive = archiver("zip", {
1869
1905
  zlib: {
1870
1906
  level: 9
@@ -1982,16 +2018,16 @@ async function deployCommand(folderPath, options = {}) {
1982
2018
  }
1983
2019
  const apiUrl = await getApiUrl();
1984
2020
  debug3("API URL:", apiUrl);
1985
- const absolutePath = path9.resolve(process.cwd(), folderPath);
1986
- if (!await fs9.pathExists(absolutePath)) {
2021
+ const absolutePath = path10.resolve(process.cwd(), folderPath);
2022
+ if (!await fs10.pathExists(absolutePath)) {
1987
2023
  logger.error(`Folder not found: ${absolutePath}`);
1988
2024
  process.exit(1);
1989
2025
  }
1990
- const hasMainTs = await fs9.pathExists(path9.join(absolutePath, "main.ts"));
1991
- const hasPackageJson = await fs9.pathExists(path9.join(absolutePath, "package.json"));
1992
- const hasMainPy = await fs9.pathExists(path9.join(absolutePath, "main.py"));
1993
- const hasRequirementsTxt = await fs9.pathExists(path9.join(absolutePath, "requirements.txt"));
1994
- const hasPyprojectToml = await fs9.pathExists(path9.join(absolutePath, "pyproject.toml"));
2026
+ const hasMainTs = await fs10.pathExists(path10.join(absolutePath, "main.ts"));
2027
+ const hasPackageJson = await fs10.pathExists(path10.join(absolutePath, "package.json"));
2028
+ const hasMainPy = await fs10.pathExists(path10.join(absolutePath, "main.py"));
2029
+ const hasRequirementsTxt = await fs10.pathExists(path10.join(absolutePath, "requirements.txt"));
2030
+ const hasPyprojectToml = await fs10.pathExists(path10.join(absolutePath, "pyproject.toml"));
1995
2031
  const isNodeProject = hasMainTs || hasPackageJson;
1996
2032
  const isPythonProject = hasMainPy || hasRequirementsTxt || hasPyprojectToml;
1997
2033
  if (!isNodeProject && !isPythonProject) {
@@ -2063,10 +2099,10 @@ Generated project name: ${chalk.bold(projectName)}
2063
2099
  } catch (e) {
2064
2100
  debug3("Could not fetch existing projects");
2065
2101
  }
2066
- let folderName = path9.basename(absolutePath);
2102
+ let folderName = path10.basename(absolutePath);
2067
2103
  if (hasPackageJson) {
2068
2104
  try {
2069
- const pkg2 = await fs9.readJSON(path9.join(absolutePath, "package.json"));
2105
+ const pkg2 = await fs10.readJSON(path10.join(absolutePath, "package.json"));
2070
2106
  folderName = pkg2.name || folderName;
2071
2107
  } catch (e) {
2072
2108
  }
@@ -2217,7 +2253,7 @@ ${error instanceof Error ? error.message : String(error)}`);
2217
2253
  }
2218
2254
  const uploadSpinner = ora5("Packaging and uploading...").start();
2219
2255
  try {
2220
- const tempZip = path9.join(os3.tmpdir(), `leanmcp-${Date.now()}.zip`);
2256
+ const tempZip = path10.join(os3.tmpdir(), `leanmcp-${Date.now()}.zip`);
2221
2257
  const zipSize = await createZipArchive(absolutePath, tempZip);
2222
2258
  uploadSpinner.text = `Packaging... (${Math.round(zipSize / 1024)}KB)`;
2223
2259
  debug3("Step 2a: Getting upload URL for project:", projectId);
@@ -2244,7 +2280,7 @@ ${error instanceof Error ? error.message : String(error)}`);
2244
2280
  throw new Error("Backend did not return upload URL");
2245
2281
  }
2246
2282
  debug3("Step 2b: Uploading to S3...");
2247
- const zipBuffer = await fs9.readFile(tempZip);
2283
+ const zipBuffer = await fs10.readFile(tempZip);
2248
2284
  const s3Response = await fetch(uploadUrl, {
2249
2285
  method: "PUT",
2250
2286
  body: zipBuffer,
@@ -2267,7 +2303,7 @@ ${error instanceof Error ? error.message : String(error)}`);
2267
2303
  s3Location
2268
2304
  })
2269
2305
  });
2270
- await fs9.remove(tempZip);
2306
+ await fs10.remove(tempZip);
2271
2307
  uploadSpinner.succeed("Project uploaded");
2272
2308
  } catch (error) {
2273
2309
  uploadSpinner.fail("Failed to upload");
@@ -2404,8 +2440,273 @@ ${error instanceof Error ? error.message : String(error)}`);
2404
2440
  }
2405
2441
  __name(deployCommand, "deployCommand");
2406
2442
 
2407
- // src/commands/projects.ts
2443
+ // src/commands/feedback.ts
2408
2444
  import ora6 from "ora";
2445
+ import fs11 from "fs-extra";
2446
+ import path11 from "path";
2447
+ import os4 from "os";
2448
+ var DEBUG_MODE4 = false;
2449
+ function debug4(message, ...args) {
2450
+ if (DEBUG_MODE4) {
2451
+ console.log(chalk.gray(`[DEBUG] ${message}`), ...args);
2452
+ }
2453
+ }
2454
+ __name(debug4, "debug");
2455
+ async function readFileAsBase64(filePath) {
2456
+ try {
2457
+ const absolutePath = path11.resolve(filePath);
2458
+ const stats = await fs11.stat(absolutePath);
2459
+ if (!stats.isFile()) {
2460
+ throw new Error(`${filePath} is not a file`);
2461
+ }
2462
+ const content = await fs11.readFile(absolutePath, "base64");
2463
+ const ext = path11.extname(absolutePath).toLowerCase();
2464
+ let mimeType = "application/octet-stream";
2465
+ switch (ext) {
2466
+ case ".txt":
2467
+ case ".log":
2468
+ mimeType = "text/plain";
2469
+ break;
2470
+ case ".json":
2471
+ mimeType = "application/json";
2472
+ break;
2473
+ case ".js":
2474
+ mimeType = "application/javascript";
2475
+ break;
2476
+ case ".ts":
2477
+ mimeType = "application/typescript";
2478
+ break;
2479
+ case ".md":
2480
+ mimeType = "text/markdown";
2481
+ break;
2482
+ }
2483
+ return {
2484
+ content,
2485
+ size: stats.size,
2486
+ type: mimeType
2487
+ };
2488
+ } catch (error) {
2489
+ throw new Error(`Failed to read file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
2490
+ }
2491
+ }
2492
+ __name(readFileAsBase64, "readFileAsBase64");
2493
+ async function collectLogFiles() {
2494
+ const attachments = [];
2495
+ const logLocations = [
2496
+ path11.join(os4.homedir(), ".leanmcp", "logs"),
2497
+ path11.join(process.cwd(), "logs"),
2498
+ path11.join(process.cwd(), ".leanmcp", "logs")
2499
+ ];
2500
+ for (const logDir of logLocations) {
2501
+ try {
2502
+ if (await fs11.pathExists(logDir)) {
2503
+ const files = await fs11.readdir(logDir);
2504
+ const logFiles = [];
2505
+ for (const file of files) {
2506
+ if (!file.endsWith(".log")) continue;
2507
+ try {
2508
+ const filePath = path11.join(logDir, file);
2509
+ const stats = await fs11.stat(filePath);
2510
+ logFiles.push({
2511
+ file,
2512
+ filePath,
2513
+ mtime: stats.mtimeMs
2514
+ });
2515
+ } catch (e) {
2516
+ }
2517
+ }
2518
+ const recentLogs = logFiles.sort((a, b) => b.mtime - a.mtime).slice(0, 3);
2519
+ for (const log of recentLogs) {
2520
+ try {
2521
+ const fileData = await readFileAsBase64(log.filePath);
2522
+ if (!attachments.some((a) => a.name === log.file)) {
2523
+ attachments.push({
2524
+ name: log.file,
2525
+ ...fileData
2526
+ });
2527
+ }
2528
+ } catch (error) {
2529
+ debug4(`Failed to read log file ${log.filePath}: ${error}`);
2530
+ }
2531
+ }
2532
+ }
2533
+ } catch (error) {
2534
+ debug4(`Failed to scan log directory ${logDir}: ${error}`);
2535
+ }
2536
+ }
2537
+ const npmDebugLog = path11.join(os4.homedir(), ".npm", "_logs");
2538
+ try {
2539
+ if (await fs11.pathExists(npmDebugLog)) {
2540
+ const logFiles = await fs11.readdir(npmDebugLog);
2541
+ const latestLog = logFiles.filter((file) => file.endsWith(".log")).sort().pop();
2542
+ if (latestLog) {
2543
+ const filePath = path11.join(npmDebugLog, latestLog);
2544
+ try {
2545
+ const fileData = await readFileAsBase64(filePath);
2546
+ attachments.push({
2547
+ name: `npm-${latestLog}`,
2548
+ ...fileData
2549
+ });
2550
+ } catch (error) {
2551
+ debug4(`Failed to read npm debug log: ${error}`);
2552
+ }
2553
+ }
2554
+ }
2555
+ } catch (error) {
2556
+ debug4(`Failed to collect npm debug logs: ${error}`);
2557
+ }
2558
+ return attachments;
2559
+ }
2560
+ __name(collectLogFiles, "collectLogFiles");
2561
+ async function sendFeedbackToApi(message, attachments = [], isAnonymous = false) {
2562
+ const apiUrl = await getApiUrl();
2563
+ const endpoint = isAnonymous ? "/feedback/anonymous" : "/feedback";
2564
+ const url = `${apiUrl}${endpoint}`;
2565
+ debug4("API URL:", apiUrl);
2566
+ debug4("Endpoint:", endpoint);
2567
+ debug4("Message length:", message.length);
2568
+ debug4("Attachments count:", attachments.length);
2569
+ const headers = {
2570
+ "Content-Type": "application/json"
2571
+ };
2572
+ if (!isAnonymous) {
2573
+ const apiKey = await getApiKey();
2574
+ if (!apiKey) {
2575
+ throw new Error("Not authenticated. Please run `leanmcp login` first.");
2576
+ }
2577
+ headers["Authorization"] = `Bearer ${apiKey}`;
2578
+ }
2579
+ const payload = {
2580
+ message,
2581
+ attachments: attachments.map((att) => ({
2582
+ name: att.name,
2583
+ content: att.content,
2584
+ size: att.size,
2585
+ type: att.type
2586
+ }))
2587
+ };
2588
+ debug4("Sending feedback request...");
2589
+ const response = await fetch(url, {
2590
+ method: "POST",
2591
+ headers,
2592
+ body: JSON.stringify(payload)
2593
+ });
2594
+ debug4("Response status:", response.status);
2595
+ debug4("Response ok:", response.ok);
2596
+ if (!response.ok) {
2597
+ const errorText = await response.text();
2598
+ debug4("Error response:", errorText);
2599
+ if (response.status === 401) {
2600
+ throw new Error("Authentication failed. Please run `leanmcp login` to re-authenticate.");
2601
+ } else if (response.status === 413) {
2602
+ throw new Error("Attachments too large. Please try again without log files.");
2603
+ } else {
2604
+ throw new Error(`Failed to send feedback: ${response.status} ${response.statusText}`);
2605
+ }
2606
+ }
2607
+ return await response.json();
2608
+ }
2609
+ __name(sendFeedbackToApi, "sendFeedbackToApi");
2610
+ async function sendFeedbackCommand(message, options) {
2611
+ logger.info("\nLeanMCP Feedback\n");
2612
+ const isAnonymous = options.anon || false;
2613
+ const includeLogs = options.includeLogs || false;
2614
+ debug4("Feedback options:", {
2615
+ isAnonymous,
2616
+ includeLogs
2617
+ });
2618
+ let feedbackMessage = message;
2619
+ if (!feedbackMessage && !process.stdin.isTTY) {
2620
+ debug4("Reading feedback message from stdin...");
2621
+ feedbackMessage = await new Promise((resolve) => {
2622
+ let data = "";
2623
+ process.stdin.on("data", (chunk) => {
2624
+ data += chunk;
2625
+ });
2626
+ process.stdin.on("end", () => {
2627
+ resolve(data.trim());
2628
+ });
2629
+ });
2630
+ }
2631
+ if (!feedbackMessage || feedbackMessage.trim().length === 0) {
2632
+ logger.error("Feedback message cannot be empty.");
2633
+ logger.info("Usage examples:");
2634
+ logger.info(' leanmcp send-feedback "Your message"');
2635
+ logger.gray(" leanmcp send-feedback << EOF");
2636
+ logger.gray(" multi-line");
2637
+ logger.gray(" message");
2638
+ logger.gray(" EOF");
2639
+ logger.info(' leanmcp send-feedback --anon "Anonymous feedback"');
2640
+ logger.info(' leanmcp send-feedback "Issue with deploy" --include-logs');
2641
+ process.exit(1);
2642
+ }
2643
+ if (feedbackMessage.length > 5e3) {
2644
+ logger.error("Feedback message is too long (max 5000 characters).");
2645
+ process.exit(1);
2646
+ }
2647
+ if (!isAnonymous) {
2648
+ const apiKey = await getApiKey();
2649
+ if (!apiKey) {
2650
+ logger.error("need to login");
2651
+ logger.info("Please run `leanmcp login` to authenticate, or use `--anon` for anonymous feedback.");
2652
+ process.exit(1);
2653
+ }
2654
+ }
2655
+ let attachments = [];
2656
+ if (includeLogs) {
2657
+ const spinner2 = ora6("Collecting log files...").start();
2658
+ try {
2659
+ attachments = await collectLogFiles();
2660
+ spinner2.succeed(`Collected ${attachments.length} log file(s)`);
2661
+ if (attachments.length > 0) {
2662
+ logger.log("Log files:", chalk.gray);
2663
+ attachments.forEach((att) => {
2664
+ logger.log(` - ${att.name} (${(att.size / 1024).toFixed(1)} KB)`, chalk.gray);
2665
+ });
2666
+ logger.log("");
2667
+ } else {
2668
+ logger.log("No log files found.", chalk.gray);
2669
+ logger.log("");
2670
+ }
2671
+ } catch (error) {
2672
+ spinner2.fail("Failed to collect log files");
2673
+ debug4("Log collection error:", error);
2674
+ logger.warn("Continuing without log files...");
2675
+ }
2676
+ }
2677
+ const spinner = ora6("Sending feedback...").start();
2678
+ try {
2679
+ const result = await sendFeedbackToApi(feedbackMessage, attachments, isAnonymous);
2680
+ spinner.succeed("Feedback sent successfully!");
2681
+ logger.success("\nThank you for your feedback!");
2682
+ logger.log(`Feedback ID: ${result.id}`, chalk.gray);
2683
+ if (isAnonymous) {
2684
+ logger.log("Type: Anonymous", chalk.gray);
2685
+ } else {
2686
+ logger.log("Type: Authenticated", chalk.gray);
2687
+ }
2688
+ if (attachments.length > 0) {
2689
+ logger.log(`Attachments: ${attachments.length}`, chalk.gray);
2690
+ }
2691
+ logger.log("\nWe appreciate your input and will review it soon.", chalk.cyan);
2692
+ } catch (error) {
2693
+ spinner.fail("Failed to send feedback");
2694
+ if (error instanceof Error) {
2695
+ logger.error(`
2696
+ ${error.message}`);
2697
+ } else {
2698
+ logger.error("\nAn unknown error occurred.");
2699
+ }
2700
+ if (DEBUG_MODE4) {
2701
+ debug4("Full error:", error);
2702
+ }
2703
+ process.exit(1);
2704
+ }
2705
+ }
2706
+ __name(sendFeedbackCommand, "sendFeedbackCommand");
2707
+
2708
+ // src/commands/projects.ts
2709
+ import ora7 from "ora";
2409
2710
  var API_ENDPOINT = "/api/projects";
2410
2711
  async function projectsListCommand() {
2411
2712
  const apiKey = await getApiKey();
@@ -2414,7 +2715,7 @@ async function projectsListCommand() {
2414
2715
  logger.gray("Run `leanmcp login` first to authenticate.\n");
2415
2716
  process.exit(1);
2416
2717
  }
2417
- const spinner = ora6("Fetching projects...").start();
2718
+ const spinner = ora7("Fetching projects...").start();
2418
2719
  try {
2419
2720
  const apiUrl = await getApiUrl();
2420
2721
  const response = await fetch(`${apiUrl}${API_ENDPOINT}`, {
@@ -2459,7 +2760,7 @@ async function projectsGetCommand(projectId) {
2459
2760
  logger.gray("Run `leanmcp login` first to authenticate.\n");
2460
2761
  process.exit(1);
2461
2762
  }
2462
- const spinner = ora6("Fetching project...").start();
2763
+ const spinner = ora7("Fetching project...").start();
2463
2764
  try {
2464
2765
  const apiUrl = await getApiUrl();
2465
2766
  const response = await fetch(`${apiUrl}${API_ENDPOINT}/${projectId}`, {
@@ -2514,7 +2815,7 @@ async function projectsDeleteCommand(projectId, options = {}) {
2514
2815
  return;
2515
2816
  }
2516
2817
  }
2517
- const spinner = ora6("Deleting project...").start();
2818
+ const spinner = ora7("Deleting project...").start();
2518
2819
  try {
2519
2820
  const apiUrl = await getApiUrl();
2520
2821
  const response = await fetch(`${apiUrl}${API_ENDPOINT}/${projectId}`, {
@@ -2541,50 +2842,50 @@ ${error instanceof Error ? error.message : String(error)}`);
2541
2842
  __name(projectsDeleteCommand, "projectsDeleteCommand");
2542
2843
 
2543
2844
  // src/commands/env.ts
2544
- import ora7 from "ora";
2545
- import path10 from "path";
2546
- import fs10 from "fs-extra";
2845
+ import ora8 from "ora";
2846
+ import path12 from "path";
2847
+ import fs12 from "fs-extra";
2547
2848
  import { confirm as confirm3 } from "@inquirer/prompts";
2548
- var DEBUG_MODE4 = false;
2849
+ var DEBUG_MODE5 = false;
2549
2850
  function setEnvDebugMode(enabled) {
2550
- DEBUG_MODE4 = enabled;
2851
+ DEBUG_MODE5 = enabled;
2551
2852
  }
2552
2853
  __name(setEnvDebugMode, "setEnvDebugMode");
2553
- function debug4(message, ...args) {
2554
- if (DEBUG_MODE4) {
2854
+ function debug5(message, ...args) {
2855
+ if (DEBUG_MODE5) {
2555
2856
  console.log(chalk.gray(`[DEBUG] ${message}`), ...args);
2556
2857
  }
2557
2858
  }
2558
- __name(debug4, "debug");
2859
+ __name(debug5, "debug");
2559
2860
  async function debugFetch2(url, options = {}) {
2560
- debug4(`HTTP ${options.method || "GET"} ${url}`);
2861
+ debug5(`HTTP ${options.method || "GET"} ${url}`);
2561
2862
  if (options.body && typeof options.body === "string") {
2562
2863
  try {
2563
2864
  const body = JSON.parse(options.body);
2564
- debug4("Request body:", JSON.stringify(body, null, 2));
2865
+ debug5("Request body:", JSON.stringify(body, null, 2));
2565
2866
  } catch {
2566
- debug4("Request body:", options.body);
2867
+ debug5("Request body:", options.body);
2567
2868
  }
2568
2869
  }
2569
2870
  const startTime = Date.now();
2570
2871
  const response = await fetch(url, options);
2571
2872
  const duration = Date.now() - startTime;
2572
- debug4(`Response: ${response.status} ${response.statusText} (${duration}ms)`);
2873
+ debug5(`Response: ${response.status} ${response.statusText} (${duration}ms)`);
2573
2874
  return response;
2574
2875
  }
2575
2876
  __name(debugFetch2, "debugFetch");
2576
2877
  var LEANMCP_CONFIG_DIR2 = ".leanmcp";
2577
2878
  var LEANMCP_CONFIG_FILE2 = "config.json";
2578
2879
  async function readLeanMCPConfig2(projectPath) {
2579
- const configPath = path10.join(projectPath, LEANMCP_CONFIG_DIR2, LEANMCP_CONFIG_FILE2);
2880
+ const configPath = path12.join(projectPath, LEANMCP_CONFIG_DIR2, LEANMCP_CONFIG_FILE2);
2580
2881
  try {
2581
- if (await fs10.pathExists(configPath)) {
2582
- const config = await fs10.readJSON(configPath);
2583
- debug4("Found existing .leanmcp config:", config);
2882
+ if (await fs12.pathExists(configPath)) {
2883
+ const config = await fs12.readJSON(configPath);
2884
+ debug5("Found existing .leanmcp config:", config);
2584
2885
  return config;
2585
2886
  }
2586
2887
  } catch (e) {
2587
- debug4("Could not read .leanmcp config:", e);
2888
+ debug5("Could not read .leanmcp config:", e);
2588
2889
  }
2589
2890
  return null;
2590
2891
  }
@@ -2597,7 +2898,7 @@ async function getDeploymentContext(folderPath) {
2597
2898
  return null;
2598
2899
  }
2599
2900
  const apiUrl = await getApiUrl();
2600
- const absolutePath = path10.resolve(process.cwd(), folderPath);
2901
+ const absolutePath = path12.resolve(process.cwd(), folderPath);
2601
2902
  const config = await readLeanMCPConfig2(absolutePath);
2602
2903
  if (!config) {
2603
2904
  logger.error("No deployment found.");
@@ -2624,7 +2925,7 @@ async function envListCommand(folderPath, options = {}) {
2624
2925
  process.exit(1);
2625
2926
  }
2626
2927
  const { apiKey, apiUrl, config } = context;
2627
- const spinner = ora7("Fetching environment variables...").start();
2928
+ const spinner = ora8("Fetching environment variables...").start();
2628
2929
  try {
2629
2930
  const reveal = options.reveal ? "?reveal=true" : "";
2630
2931
  const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env${reveal}`, {
@@ -2665,7 +2966,7 @@ async function envSetCommand(keyValue, folderPath, options = {}) {
2665
2966
  const { apiKey, apiUrl, config } = context;
2666
2967
  let variables = {};
2667
2968
  if (options.file) {
2668
- const spinner2 = ora7(`Loading from ${options.file}...`).start();
2969
+ const spinner2 = ora8(`Loading from ${options.file}...`).start();
2669
2970
  try {
2670
2971
  variables = await loadEnvFile(options.file);
2671
2972
  spinner2.succeed(`Loaded ${Object.keys(variables).length} variable(s) from ${options.file}`);
@@ -2706,7 +3007,7 @@ ${error instanceof Error ? error.message : String(error)}`);
2706
3007
  logger.warn("No variables to set.\n");
2707
3008
  return;
2708
3009
  }
2709
- const spinner = ora7("Updating environment variables...").start();
3010
+ const spinner = ora8("Updating environment variables...").start();
2710
3011
  try {
2711
3012
  const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env`, {
2712
3013
  method: "PUT",
@@ -2791,7 +3092,7 @@ async function envRemoveCommand(key, folderPath, options = {}) {
2791
3092
  return;
2792
3093
  }
2793
3094
  }
2794
- const spinner = ora7(`Removing ${key}...`).start();
3095
+ const spinner = ora8(`Removing ${key}...`).start();
2795
3096
  try {
2796
3097
  const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env/${key}`, {
2797
3098
  method: "DELETE",
@@ -2820,7 +3121,7 @@ async function envPullCommand(folderPath, options = {}) {
2820
3121
  }
2821
3122
  const { apiKey, apiUrl, config } = context;
2822
3123
  const outputFile = options.file || ".env.remote";
2823
- const spinner = ora7("Fetching environment variables...").start();
3124
+ const spinner = ora8("Fetching environment variables...").start();
2824
3125
  try {
2825
3126
  const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env?reveal=true`, {
2826
3127
  headers: {
@@ -2859,7 +3160,7 @@ async function envPushCommand(folderPath, options = {}) {
2859
3160
  }
2860
3161
  const { apiKey, apiUrl, config } = context;
2861
3162
  const inputFile = options.file || ".env";
2862
- const loadSpinner = ora7(`Loading from ${inputFile}...`).start();
3163
+ const loadSpinner = ora8(`Loading from ${inputFile}...`).start();
2863
3164
  let variables;
2864
3165
  try {
2865
3166
  variables = await loadEnvFile(inputFile);
@@ -2886,7 +3187,7 @@ ${error instanceof Error ? error.message : String(error)}`);
2886
3187
  return;
2887
3188
  }
2888
3189
  }
2889
- const pushSpinner = ora7("Pushing environment variables...").start();
3190
+ const pushSpinner = ora8("Pushing environment variables...").start();
2890
3191
  try {
2891
3192
  const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env`, {
2892
3193
  method: "PUT",
@@ -3733,30 +4034,30 @@ program.command("create <projectName>").description("Create a new LeanMCP projec
3733
4034
  projectName,
3734
4035
  ...options
3735
4036
  });
3736
- const spinner = ora8(`Creating project ${projectName}...`).start();
3737
- const targetDir = path11.join(process.cwd(), projectName);
3738
- if (fs11.existsSync(targetDir)) {
4037
+ const spinner = ora9(`Creating project ${projectName}...`).start();
4038
+ const targetDir = path13.join(process.cwd(), projectName);
4039
+ if (fs13.existsSync(targetDir)) {
3739
4040
  spinner.fail(`Folder ${projectName} already exists.`);
3740
4041
  process.exit(1);
3741
4042
  }
3742
- await fs11.mkdirp(targetDir);
4043
+ await fs13.mkdirp(targetDir);
3743
4044
  const isPython = options.python === true;
3744
4045
  if (isPython) {
3745
4046
  const requirements = getPythonRequirementsTemplate();
3746
- await fs11.writeFile(path11.join(targetDir, "requirements.txt"), requirements);
4047
+ await fs13.writeFile(path13.join(targetDir, "requirements.txt"), requirements);
3747
4048
  const mainPy = getPythonMainTemplate(projectName);
3748
- await fs11.writeFile(path11.join(targetDir, "main.py"), mainPy);
3749
- await fs11.writeFile(path11.join(targetDir, ".gitignore"), pythonGitignoreTemplate);
4049
+ await fs13.writeFile(path13.join(targetDir, "main.py"), mainPy);
4050
+ await fs13.writeFile(path13.join(targetDir, ".gitignore"), pythonGitignoreTemplate);
3750
4051
  const env = `# Server Configuration
3751
4052
  PORT=3001
3752
4053
 
3753
4054
  # Add your environment variables here
3754
4055
  `;
3755
- await fs11.writeFile(path11.join(targetDir, ".env"), env);
4056
+ await fs13.writeFile(path13.join(targetDir, ".env"), env);
3756
4057
  const readme = getPythonReadmeTemplate(projectName);
3757
- await fs11.writeFile(path11.join(targetDir, "README.md"), readme);
4058
+ await fs13.writeFile(path13.join(targetDir, "README.md"), readme);
3758
4059
  } else {
3759
- await fs11.mkdirp(path11.join(targetDir, "mcp", "example"));
4060
+ await fs13.mkdirp(path13.join(targetDir, "mcp", "example"));
3760
4061
  const pkg2 = {
3761
4062
  name: projectName,
3762
4063
  version: "1.0.0",
@@ -3792,7 +4093,7 @@ PORT=3001
3792
4093
  typescript: "^5.6.3"
3793
4094
  }
3794
4095
  };
3795
- await fs11.writeJSON(path11.join(targetDir, "package.json"), pkg2, {
4096
+ await fs13.writeJSON(path13.join(targetDir, "package.json"), pkg2, {
3796
4097
  spaces: 2
3797
4098
  });
3798
4099
  const tsconfig = {
@@ -3815,15 +4116,15 @@ PORT=3001
3815
4116
  "dist"
3816
4117
  ]
3817
4118
  };
3818
- await fs11.writeJSON(path11.join(targetDir, "tsconfig.json"), tsconfig, {
4119
+ await fs13.writeJSON(path13.join(targetDir, "tsconfig.json"), tsconfig, {
3819
4120
  spaces: 2
3820
4121
  });
3821
4122
  const dashboardLine = options.dashboard === false ? `
3822
4123
  dashboard: false, // Dashboard disabled via --no-dashboard` : "";
3823
4124
  const mainTs = getMainTsTemplate(projectName, dashboardLine);
3824
- await fs11.writeFile(path11.join(targetDir, "main.ts"), mainTs);
4125
+ await fs13.writeFile(path13.join(targetDir, "main.ts"), mainTs);
3825
4126
  const exampleServiceTs = getExampleServiceTemplate(projectName);
3826
- await fs11.writeFile(path11.join(targetDir, "mcp", "example", "index.ts"), exampleServiceTs);
4127
+ await fs13.writeFile(path13.join(targetDir, "mcp", "example", "index.ts"), exampleServiceTs);
3827
4128
  const gitignore = gitignoreTemplate;
3828
4129
  const env = `# Server Configuration
3829
4130
  PORT=3001
@@ -3831,10 +4132,10 @@ NODE_ENV=development
3831
4132
 
3832
4133
  # Add your environment variables here
3833
4134
  `;
3834
- await fs11.writeFile(path11.join(targetDir, ".gitignore"), gitignore);
3835
- await fs11.writeFile(path11.join(targetDir, ".env"), env);
4135
+ await fs13.writeFile(path13.join(targetDir, ".gitignore"), gitignore);
4136
+ await fs13.writeFile(path13.join(targetDir, ".env"), env);
3836
4137
  const readme = getReadmeTemplate(projectName);
3837
- await fs11.writeFile(path11.join(targetDir, "README.md"), readme);
4138
+ await fs13.writeFile(path13.join(targetDir, "README.md"), readme);
3838
4139
  }
3839
4140
  spinner.succeed(`Project ${projectName} created!`);
3840
4141
  logger.log("\nSuccess! Your MCP server is ready.\n", chalk.green);
@@ -3881,7 +4182,7 @@ NODE_ENV=development
3881
4182
  default: true
3882
4183
  });
3883
4184
  if (shouldInstall) {
3884
- const installSpinner = ora8("Installing dependencies...").start();
4185
+ const installSpinner = ora9("Installing dependencies...").start();
3885
4186
  try {
3886
4187
  await new Promise((resolve, reject) => {
3887
4188
  const npmInstall = spawn4("npm", [
@@ -3954,24 +4255,33 @@ NODE_ENV=development
3954
4255
  logger.log("To deploy to LeanMCP cloud:", chalk.cyan);
3955
4256
  logger.log(` cd ${projectName}`, chalk.gray);
3956
4257
  logger.log(` leanmcp deploy .`, chalk.gray);
4258
+ logger.log("\nSend us feedback:", chalk.cyan);
4259
+ logger.log(' leanmcp send-feedback "Great tool!"\n', chalk.gray);
3957
4260
  }
3958
4261
  });
4262
+ program.command("send-feedback [message]").description("Send feedback to the LeanMCP team").option("--anon", "Send feedback anonymously").option("--include-logs", "Include local log files with feedback").action(async (message, options) => {
4263
+ trackCommand("send-feedback", {
4264
+ hasMessage: !!message,
4265
+ ...options
4266
+ });
4267
+ await sendFeedbackCommand(message, options);
4268
+ });
3959
4269
  program.command("add <serviceName>").description("Add a new MCP service to your project").action(async (serviceName) => {
3960
4270
  const cwd = process.cwd();
3961
- const mcpDir = path11.join(cwd, "mcp");
3962
- if (!fs11.existsSync(path11.join(cwd, "main.ts"))) {
4271
+ const mcpDir = path13.join(cwd, "mcp");
4272
+ if (!fs13.existsSync(path13.join(cwd, "main.ts"))) {
3963
4273
  logger.log("ERROR: Not a LeanMCP project (main.ts missing).", chalk.red);
3964
4274
  process.exit(1);
3965
4275
  }
3966
- const serviceDir = path11.join(mcpDir, serviceName);
3967
- const serviceFile = path11.join(serviceDir, "index.ts");
3968
- if (fs11.existsSync(serviceDir)) {
4276
+ const serviceDir = path13.join(mcpDir, serviceName);
4277
+ const serviceFile = path13.join(serviceDir, "index.ts");
4278
+ if (fs13.existsSync(serviceDir)) {
3969
4279
  logger.log(`ERROR: Service ${serviceName} already exists.`, chalk.red);
3970
4280
  process.exit(1);
3971
4281
  }
3972
- await fs11.mkdirp(serviceDir);
4282
+ await fs13.mkdirp(serviceDir);
3973
4283
  const indexTs = getServiceIndexTemplate(serviceName, capitalize(serviceName));
3974
- await fs11.writeFile(serviceFile, indexTs);
4284
+ await fs13.writeFile(serviceFile, indexTs);
3975
4285
  logger.log(`\\nCreated new service: ${chalk.bold(serviceName)}`, chalk.green);
3976
4286
  logger.log(` File: mcp/${serviceName}/index.ts`, chalk.gray);
3977
4287
  logger.log(` Tool: greet`, chalk.gray);