@tostudy-ai/mcp-setup 1.3.0 → 1.4.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.
Files changed (94) hide show
  1. package/README.md +26 -25
  2. package/dist/index.d.ts +2 -14
  3. package/dist/index.js +2983 -613
  4. package/dist/index.js.map +1 -1
  5. package/package.json +5 -2
  6. package/dist/__tests__/e2e-diagnostic-repair-flow.test.d.ts +0 -52
  7. package/dist/__tests__/e2e-diagnostic-repair-flow.test.d.ts.map +0 -1
  8. package/dist/__tests__/e2e-diagnostic-repair-flow.test.js +0 -720
  9. package/dist/__tests__/e2e-diagnostic-repair-flow.test.js.map +0 -1
  10. package/dist/__tests__/e2e-wizard-flow.test.d.ts +0 -43
  11. package/dist/__tests__/e2e-wizard-flow.test.d.ts.map +0 -1
  12. package/dist/__tests__/e2e-wizard-flow.test.js +0 -418
  13. package/dist/__tests__/e2e-wizard-flow.test.js.map +0 -1
  14. package/dist/__tests__/ide-handlers.test.d.ts +0 -10
  15. package/dist/__tests__/ide-handlers.test.d.ts.map +0 -1
  16. package/dist/__tests__/ide-handlers.test.js +0 -358
  17. package/dist/__tests__/ide-handlers.test.js.map +0 -1
  18. package/dist/__tests__/install-command.test.d.ts +0 -10
  19. package/dist/__tests__/install-command.test.d.ts.map +0 -1
  20. package/dist/__tests__/install-command.test.js +0 -248
  21. package/dist/__tests__/install-command.test.js.map +0 -1
  22. package/dist/callback-page.d.ts +0 -6
  23. package/dist/callback-page.d.ts.map +0 -1
  24. package/dist/callback-page.js +0 -96
  25. package/dist/callback-page.js.map +0 -1
  26. package/dist/config.d.ts +0 -62
  27. package/dist/config.d.ts.map +0 -1
  28. package/dist/config.js +0 -167
  29. package/dist/config.js.map +0 -1
  30. package/dist/detect.d.ts +0 -42
  31. package/dist/detect.d.ts.map +0 -1
  32. package/dist/detect.js +0 -277
  33. package/dist/detect.js.map +0 -1
  34. package/dist/diagnose.d.ts +0 -36
  35. package/dist/diagnose.d.ts.map +0 -1
  36. package/dist/diagnose.js +0 -502
  37. package/dist/diagnose.js.map +0 -1
  38. package/dist/ide-handlers/antigravity.d.ts +0 -16
  39. package/dist/ide-handlers/antigravity.d.ts.map +0 -1
  40. package/dist/ide-handlers/antigravity.js +0 -46
  41. package/dist/ide-handlers/antigravity.js.map +0 -1
  42. package/dist/ide-handlers/base.d.ts +0 -36
  43. package/dist/ide-handlers/base.d.ts.map +0 -1
  44. package/dist/ide-handlers/base.js +0 -66
  45. package/dist/ide-handlers/base.js.map +0 -1
  46. package/dist/ide-handlers/claude-code.d.ts +0 -15
  47. package/dist/ide-handlers/claude-code.d.ts.map +0 -1
  48. package/dist/ide-handlers/claude-code.js +0 -50
  49. package/dist/ide-handlers/claude-code.js.map +0 -1
  50. package/dist/ide-handlers/codex.d.ts +0 -15
  51. package/dist/ide-handlers/codex.d.ts.map +0 -1
  52. package/dist/ide-handlers/codex.js +0 -53
  53. package/dist/ide-handlers/codex.js.map +0 -1
  54. package/dist/ide-handlers/cursor.d.ts +0 -15
  55. package/dist/ide-handlers/cursor.d.ts.map +0 -1
  56. package/dist/ide-handlers/cursor.js +0 -61
  57. package/dist/ide-handlers/cursor.js.map +0 -1
  58. package/dist/ide-handlers/desktop.d.ts +0 -16
  59. package/dist/ide-handlers/desktop.d.ts.map +0 -1
  60. package/dist/ide-handlers/desktop.js +0 -50
  61. package/dist/ide-handlers/desktop.js.map +0 -1
  62. package/dist/ide-handlers/index.d.ts +0 -21
  63. package/dist/ide-handlers/index.d.ts.map +0 -1
  64. package/dist/ide-handlers/index.js +0 -55
  65. package/dist/ide-handlers/index.js.map +0 -1
  66. package/dist/ide-handlers/manual.d.ts +0 -16
  67. package/dist/ide-handlers/manual.d.ts.map +0 -1
  68. package/dist/ide-handlers/manual.js +0 -34
  69. package/dist/ide-handlers/manual.js.map +0 -1
  70. package/dist/ide-handlers/opencode.d.ts +0 -15
  71. package/dist/ide-handlers/opencode.d.ts.map +0 -1
  72. package/dist/ide-handlers/opencode.js +0 -57
  73. package/dist/ide-handlers/opencode.js.map +0 -1
  74. package/dist/ide-handlers/vscode.d.ts +0 -16
  75. package/dist/ide-handlers/vscode.d.ts.map +0 -1
  76. package/dist/ide-handlers/vscode.js +0 -62
  77. package/dist/ide-handlers/vscode.js.map +0 -1
  78. package/dist/ide-handlers/windsurf.d.ts +0 -16
  79. package/dist/ide-handlers/windsurf.d.ts.map +0 -1
  80. package/dist/ide-handlers/windsurf.js +0 -46
  81. package/dist/ide-handlers/windsurf.js.map +0 -1
  82. package/dist/index.d.ts.map +0 -1
  83. package/dist/oauth-server.d.ts +0 -4
  84. package/dist/oauth-server.d.ts.map +0 -1
  85. package/dist/oauth-server.js +0 -49
  86. package/dist/oauth-server.js.map +0 -1
  87. package/dist/prompts.d.ts +0 -23
  88. package/dist/prompts.d.ts.map +0 -1
  89. package/dist/prompts.js +0 -68
  90. package/dist/prompts.js.map +0 -1
  91. package/dist/repair.d.ts +0 -50
  92. package/dist/repair.d.ts.map +0 -1
  93. package/dist/repair.js +0 -633
  94. package/dist/repair.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,664 +1,3034 @@
1
- #!/usr/bin/env node
2
- /**
3
- * ToStudy MCP Setup CLI
4
- *
5
- * Configures Claude Code to connect to the ToStudy MCP server.
6
- *
7
- * Usage:
8
- * npx @tostudy-ai/mcp-setup
9
- * npx @tostudy-ai/mcp-setup wizard
10
- * npx @tostudy-ai/mcp-setup --api-key <key>
11
- * npx @tostudy-ai/mcp-setup --uninstall
12
- */
1
+ // src/index.ts
13
2
  import { program } from "commander";
14
- import chalk from "chalk";
15
- import { execFile } from "node:child_process";
16
- import { getClaudeConfigPath, isClaudeInstalled, addTostudyMcpServer, removeTostudyMcpServer, isTostudyMcpConfigured, } from "./config.js";
17
- import { promptApiKey, confirm, prompt } from "./prompts.js";
18
- import { getInstalledIDEs } from "./detect.js";
19
- import { runDiagnostics, printDiagnosticReport } from "./diagnose.js";
20
- import { repairAllIssues, printRepairReport } from "./repair.js";
21
- import { getIDEHandler, detectInstalledIDEs } from "./ide-handlers/index.js";
22
- import { startCallbackServer } from "./oauth-server.js";
23
- const VERSION = "1.3.0";
24
- const DEFAULT_PLATFORM_URL = "https://tostudy.ai";
25
- const OAUTH_PORT = 9877;
3
+ import chalk6 from "chalk";
4
+ import { execFile } from "child_process";
5
+
6
+ // src/config.ts
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
8
+ import { dirname, join } from "path";
9
+ import { homedir, platform } from "os";
10
+ function extractApiKey(config) {
11
+ const authHeader = config.headers?.["Authorization"];
12
+ if (authHeader?.startsWith("Bearer ")) {
13
+ return authHeader.replace("Bearer ", "");
14
+ }
15
+ if (config.args) {
16
+ for (let i = 0; i < config.args.length; i++) {
17
+ if (config.args[i] === "--header" && config.args[i + 1]?.startsWith("Authorization:Bearer ")) {
18
+ return config.args[i + 1].replace("Authorization:Bearer ", "");
19
+ }
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+ function extractMcpUrl(config) {
25
+ if (config.url) {
26
+ return config.url;
27
+ }
28
+ if (config.args && config.args[0] === "-y" && config.args[1] === "mcp-remote" && config.args[2]) {
29
+ return config.args[2];
30
+ }
31
+ return null;
32
+ }
33
+ function getClientOsHeaderValue() {
34
+ switch (platform()) {
35
+ case "darwin":
36
+ return "macos";
37
+ case "win32":
38
+ return "windows";
39
+ default:
40
+ return "linux";
41
+ }
42
+ }
43
+ function getClaudeConfigPath() {
44
+ const home = homedir();
45
+ const os = platform();
46
+ switch (os) {
47
+ case "darwin":
48
+ return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
49
+ case "win32":
50
+ return join(
51
+ process.env.APPDATA || join(home, "AppData", "Roaming"),
52
+ "Claude",
53
+ "claude_desktop_config.json"
54
+ );
55
+ case "linux":
56
+ return join(home, ".config", "Claude", "claude_desktop_config.json");
57
+ default:
58
+ throw new Error(`Unsupported operating system: ${os}`);
59
+ }
60
+ }
61
+ function isClaudeInstalled() {
62
+ const configPath = getClaudeConfigPath();
63
+ const configDir = dirname(configPath);
64
+ return existsSync(configDir);
65
+ }
66
+ function readClaudeConfig() {
67
+ const configPath = getClaudeConfigPath();
68
+ if (!existsSync(configPath)) {
69
+ return { mcpServers: {} };
70
+ }
71
+ try {
72
+ const content = readFileSync(configPath, "utf-8");
73
+ return JSON.parse(content);
74
+ } catch {
75
+ return { mcpServers: {} };
76
+ }
77
+ }
78
+ function writeClaudeConfig(config) {
79
+ const configPath = getClaudeConfigPath();
80
+ const configDir = dirname(configPath);
81
+ if (!existsSync(configDir)) {
82
+ mkdirSync(configDir, { recursive: true });
83
+ }
84
+ if (existsSync(configPath)) {
85
+ const backupPath = `${configPath}.backup`;
86
+ copyFileSync(configPath, backupPath);
87
+ }
88
+ writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
89
+ }
90
+ function addTostudyMcpServer(apiKey, platformUrl, clientInfo = {}) {
91
+ const config = readClaudeConfig();
92
+ if (!config.mcpServers) {
93
+ config.mcpServers = {};
94
+ }
95
+ const ide = clientInfo.ide || "desktop";
96
+ const os = clientInfo.os || getClientOsHeaderValue();
97
+ config.mcpServers["tostudy"] = {
98
+ command: "npx",
99
+ args: [
100
+ "-y",
101
+ "mcp-remote",
102
+ `${platformUrl}/mcp`,
103
+ "--header",
104
+ `Authorization:Bearer ${apiKey}`,
105
+ "--header",
106
+ `X-Tostudy-Client-IDE:${ide}`,
107
+ "--header",
108
+ `X-Tostudy-Client-OS:${os}`
109
+ ]
110
+ };
111
+ writeClaudeConfig(config);
112
+ }
113
+ function removeTostudyMcpServer() {
114
+ const config = readClaudeConfig();
115
+ if (config.mcpServers && config.mcpServers["tostudy"]) {
116
+ delete config.mcpServers["tostudy"];
117
+ writeClaudeConfig(config);
118
+ }
119
+ }
120
+ function isTostudyMcpConfigured() {
121
+ const config = readClaudeConfig();
122
+ return !!(config.mcpServers && config.mcpServers["tostudy"]);
123
+ }
124
+ function getTostudyMcpConfig() {
125
+ const config = readClaudeConfig();
126
+ return config.mcpServers?.["tostudy"] || null;
127
+ }
128
+
129
+ // src/prompts.ts
130
+ import * as readline from "readline";
26
131
  function println(message = "") {
27
- process.stdout.write(`${message}\n`);
132
+ process.stdout.write(`${message}
133
+ `);
28
134
  }
29
- function eprintln(message = "") {
30
- process.stderr.write(`${message}\n`);
135
+ function createInterface2() {
136
+ return readline.createInterface({
137
+ input: process.stdin,
138
+ output: process.stdout
139
+ });
31
140
  }
32
- /**
33
- * Print styled banner
34
- */
35
- function printBanner() {
36
- println();
37
- println(chalk.cyan(" ╔═══════════════════════════════════════╗"));
38
- println(chalk.cyan(" ║") + chalk.white.bold(" ToStudy MCP Setup ") + chalk.cyan("║"));
39
- println(chalk.cyan(" ╚═══════════════════════════════════════╝"));
40
- println();
41
- }
42
- /**
43
- * Validate API key with the platform
44
- */
45
- async function validateApiKey(apiKey, platformUrl) {
46
- try {
47
- const response = await fetch(`${platformUrl}/api/mcp/heartbeat`, {
48
- method: "POST",
49
- headers: {
50
- Authorization: `Bearer ${apiKey}`,
51
- "Content-Type": "application/json",
52
- },
53
- body: JSON.stringify({ timestamp: new Date().toISOString() }),
54
- });
55
- return response.ok || response.status === 204;
56
- }
57
- catch {
58
- // Network error - can't validate, but allow anyway for offline setup
59
- println(chalk.yellow("! Nao foi possivel validar a API key (servidor offline?)"));
60
- println(chalk.yellow(" A configuracao sera salva mesmo assim."));
61
- return true;
141
+ function prompt(question) {
142
+ return new Promise((resolve) => {
143
+ const rl = createInterface2();
144
+ rl.question(question, (answer) => {
145
+ rl.close();
146
+ resolve(answer.trim());
147
+ });
148
+ });
149
+ }
150
+ async function promptApiKey() {
151
+ println();
152
+ println("Cole sua API key (de /student/settings/mcp):");
153
+ const apiKey = await prompt("> ");
154
+ if (!apiKey) {
155
+ throw new Error("API key n\xE3o pode ser vazia");
156
+ }
157
+ if (apiKey.length < 32) {
158
+ throw new Error("API key parece ser muito curta. Verifique e tente novamente.");
159
+ }
160
+ return apiKey;
161
+ }
162
+ async function confirm(question, defaultValue = false) {
163
+ const defaultStr = defaultValue ? "[S/n]" : "[s/N]";
164
+ const answer = await prompt(`${question} ${defaultStr} `);
165
+ if (!answer) {
166
+ return defaultValue;
167
+ }
168
+ return answer.toLowerCase().startsWith("s") || answer.toLowerCase().startsWith("y");
169
+ }
170
+ async function promptCourseSelection(courses) {
171
+ if (courses.length === 0) {
172
+ return null;
173
+ }
174
+ println();
175
+ println("Cursos ativos:");
176
+ courses.forEach((course, index2) => {
177
+ println(` ${index2 + 1}. ${course.title} \u2014 ${course.progressPercent}% completo`);
178
+ });
179
+ const answer = await prompt(`Selecione um curso [1-${courses.length}] (default: 1): `);
180
+ if (!answer) {
181
+ return courses[0]?.id ?? null;
182
+ }
183
+ const index = Number.parseInt(answer, 10);
184
+ if (!Number.isInteger(index) || index < 1 || index > courses.length) {
185
+ println("Sele\xE7\xE3o inv\xE1lida.");
186
+ return null;
187
+ }
188
+ return courses[index - 1]?.id ?? null;
189
+ }
190
+
191
+ // src/detect.ts
192
+ import { existsSync as existsSync2, readdirSync } from "fs";
193
+ import { join as join2 } from "path";
194
+ import { homedir as homedir2, platform as platform2 } from "os";
195
+ function getVSCodeConfigDir(variant) {
196
+ const home = homedir2();
197
+ const os = platform2();
198
+ const folderNames = {
199
+ stable: "Code",
200
+ insiders: "Code - Insiders",
201
+ cursor: "Cursor"
202
+ };
203
+ const folder = folderNames[variant];
204
+ switch (os) {
205
+ case "darwin":
206
+ return join2(home, "Library", "Application Support", folder, "User");
207
+ case "win32":
208
+ return join2(process.env.APPDATA || join2(home, "AppData", "Roaming"), folder, "User");
209
+ case "linux":
210
+ return join2(home, ".config", folder, "User");
211
+ default:
212
+ return "";
213
+ }
214
+ }
215
+ function getVSCodeMcpConfigPath(variant) {
216
+ const configDir = getVSCodeConfigDir(variant);
217
+ if (!configDir) return "";
218
+ return join2(configDir, "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json");
219
+ }
220
+ function isVSCodeInstalled(variant) {
221
+ const os = platform2();
222
+ const home = homedir2();
223
+ const appNames = {
224
+ stable: ["Visual Studio Code.app", "code"],
225
+ insiders: ["Visual Studio Code - Insiders.app", "code-insiders"],
226
+ cursor: ["Cursor.app", "cursor"]
227
+ };
228
+ const [macApp, linuxCmd] = appNames[variant];
229
+ switch (os) {
230
+ case "darwin":
231
+ return existsSync2(join2("/Applications", macApp)) || existsSync2(join2(home, "Applications", macApp));
232
+ case "win32": {
233
+ const programFiles = process.env.ProgramFiles || "C:\\Program Files";
234
+ const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
235
+ const localAppData = process.env.LOCALAPPDATA || join2(home, "AppData", "Local");
236
+ const winPaths = variant === "cursor" ? [join2(localAppData, "Programs", "cursor", "Cursor.exe")] : [
237
+ join2(programFiles, "Microsoft VS Code", "Code.exe"),
238
+ join2(programFilesX86, "Microsoft VS Code", "Code.exe"),
239
+ join2(localAppData, "Programs", "Microsoft VS Code", "Code.exe")
240
+ ];
241
+ if (variant === "insiders") {
242
+ winPaths.push(
243
+ join2(programFiles, "Microsoft VS Code Insiders", "Code - Insiders.exe"),
244
+ join2(localAppData, "Programs", "Microsoft VS Code Insiders", "Code - Insiders.exe")
245
+ );
246
+ }
247
+ return winPaths.some((p) => existsSync2(p));
62
248
  }
249
+ case "linux":
250
+ return existsSync2(getVSCodeConfigDir(variant)) || existsSync2(join2("/usr/bin", linuxCmd)) || existsSync2(join2("/usr/local/bin", linuxCmd)) || existsSync2(join2(home, ".local", "bin", linuxCmd));
251
+ default:
252
+ return false;
253
+ }
63
254
  }
64
- /**
65
- * Exchange an OAuth authorization code for a JWT token.
66
- */
67
- async function exchangeCodeForToken(code, platformUrl, client = "mcp") {
68
- const res = await fetch(`${platformUrl}/api/cli/auth/exchange`, {
69
- method: "POST",
70
- headers: { "Content-Type": "application/json" },
71
- body: JSON.stringify({ code, client }),
255
+ function getJetBrainsConfigDir() {
256
+ const home = homedir2();
257
+ const os = platform2();
258
+ switch (os) {
259
+ case "darwin":
260
+ return join2(home, "Library", "Application Support", "JetBrains");
261
+ case "win32":
262
+ return join2(process.env.APPDATA || join2(home, "AppData", "Roaming"), "JetBrains");
263
+ case "linux":
264
+ return join2(home, ".config", "JetBrains");
265
+ default:
266
+ return "";
267
+ }
268
+ }
269
+ function getInstalledJetBrainsIDEs() {
270
+ const configDir = getJetBrainsConfigDir();
271
+ if (!configDir || !existsSync2(configDir)) {
272
+ return [];
273
+ }
274
+ try {
275
+ const entries = readdirSync(configDir, { withFileTypes: true });
276
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => {
277
+ return /^(IntelliJIdea|WebStorm|PyCharm|PhpStorm|GoLand|Rider|CLion|RubyMine|DataGrip|Fleet)\d{4}\.\d/i.test(name);
72
278
  });
73
- if (!res.ok) {
74
- const body = (await res.json().catch(() => ({})));
75
- throw new Error(body.error ?? `Falha na autenticacao (${res.status})`);
279
+ } catch {
280
+ return [];
281
+ }
282
+ }
283
+ function isJetBrainsInstalled() {
284
+ const installedIDEs = getInstalledJetBrainsIDEs();
285
+ return installedIDEs.length > 0;
286
+ }
287
+ function getPrimaryJetBrainsIDE() {
288
+ const installedIDEs = getInstalledJetBrainsIDEs();
289
+ if (installedIDEs.length === 0) return null;
290
+ const sorted = installedIDEs.sort((a, b) => {
291
+ const versionA = a.match(/\d{4}\.\d/)?.[0] || "0";
292
+ const versionB = b.match(/\d{4}\.\d/)?.[0] || "0";
293
+ return versionB.localeCompare(versionA);
294
+ });
295
+ return sorted[0];
296
+ }
297
+ function getJetBrainsMcpConfigPath(ideDir) {
298
+ const configDir = getJetBrainsConfigDir();
299
+ if (!configDir) return "";
300
+ const targetDir = ideDir || getPrimaryJetBrainsIDE();
301
+ if (!targetDir) return "";
302
+ return join2(configDir, targetDir, "options", "mcp-servers.json");
303
+ }
304
+ function detectVSCode() {
305
+ const installed = isVSCodeInstalled("stable");
306
+ return {
307
+ type: "vscode",
308
+ name: "Visual Studio Code",
309
+ configPath: getVSCodeMcpConfigPath("stable"),
310
+ installed
311
+ };
312
+ }
313
+ function detectVSCodeInsiders() {
314
+ const installed = isVSCodeInstalled("insiders");
315
+ return {
316
+ type: "vscode-insiders",
317
+ name: "Visual Studio Code - Insiders",
318
+ configPath: getVSCodeMcpConfigPath("insiders"),
319
+ installed
320
+ };
321
+ }
322
+ function detectCursor() {
323
+ const installed = isVSCodeInstalled("cursor");
324
+ return {
325
+ type: "cursor",
326
+ name: "Cursor",
327
+ configPath: getVSCodeMcpConfigPath("cursor"),
328
+ installed
329
+ };
330
+ }
331
+ function detectJetBrains() {
332
+ const installed = isJetBrainsInstalled();
333
+ const primaryIDE = getPrimaryJetBrainsIDE();
334
+ let name = "JetBrains IDE";
335
+ let version;
336
+ if (primaryIDE) {
337
+ const match = primaryIDE.match(/^(\w+?)(\d{4}\.\d)/);
338
+ if (match) {
339
+ const ideNames = {
340
+ "IntelliJIdea": "IntelliJ IDEA",
341
+ "WebStorm": "WebStorm",
342
+ "PyCharm": "PyCharm",
343
+ "PhpStorm": "PhpStorm",
344
+ "GoLand": "GoLand",
345
+ "Rider": "Rider",
346
+ "CLion": "CLion",
347
+ "RubyMine": "RubyMine",
348
+ "DataGrip": "DataGrip",
349
+ "Fleet": "Fleet"
350
+ };
351
+ name = ideNames[match[1]] || match[1];
352
+ version = match[2];
76
353
  }
77
- return res.json();
354
+ }
355
+ return {
356
+ type: "jetbrains",
357
+ name,
358
+ version,
359
+ configPath: getJetBrainsMcpConfigPath(),
360
+ installed
361
+ };
78
362
  }
79
- /**
80
- * Detect IDEs, let user choose which to configure, then write config.
81
- */
82
- async function configureIDEs(token, platformUrl) {
83
- const mcpUrl = resolveMcpServerUrl(platformUrl);
84
- // Exclude "manual" from auto-detection (it prints JWT to screen)
85
- const allDetected = await detectInstalledIDEs();
86
- const installedIDEs = allDetected.filter((h) => h.id !== "manual");
87
- if (installedIDEs.length === 0) {
88
- println(chalk.yellow(" Nenhuma IDE suportada detectada."));
89
- println(chalk.gray(" Use: npx @tostudy-ai/mcp-setup install --ide <nome> --key <token>"));
90
- return 0;
91
- }
92
- // Show detected IDEs and let user choose
93
- println(chalk.white(" IDEs detectadas:\n"));
94
- for (let i = 0; i < installedIDEs.length; i++) {
95
- println(` ${chalk.cyan(String(i + 1))}. ${installedIDEs[i].name}`);
96
- }
97
- println(` ${chalk.cyan("a")}. Todas`);
98
- println();
99
- const answer = await prompt(" Quais configurar? (números separados por vírgula, ou 'a' para todas) ");
100
- const trimmed = answer.trim().toLowerCase();
101
- let selectedIDEs;
102
- if (!trimmed || trimmed === "a" || trimmed === "all") {
103
- selectedIDEs = installedIDEs;
104
- }
105
- else {
106
- const indices = trimmed
107
- .split(/[,\s]+/)
108
- .map((s) => parseInt(s, 10) - 1)
109
- .filter((i) => i >= 0 && i < installedIDEs.length);
110
- if (indices.length === 0) {
111
- println(chalk.yellow(" Nenhuma IDE selecionada."));
112
- return 0;
113
- }
114
- selectedIDEs = indices.map((i) => installedIDEs[i]);
115
- }
116
- println();
117
- // For Claude Code, ask global vs local
118
- const claudeCodeHandler = selectedIDEs.find((h) => h.id === "claude-code");
119
- let claudeCodeScope = "user";
120
- if (claudeCodeHandler) {
121
- const scopeAnswer = await prompt(" Claude Code: configurar global ou local? (g/l, default: global) ");
122
- claudeCodeScope = scopeAnswer.trim().toLowerCase().startsWith("l") ? "project" : "user";
123
- println();
124
- }
125
- let configured = 0;
126
- for (const handler of selectedIDEs) {
127
- process.stdout.write(` ${chalk.gray("Configurando")} ${handler.name}... `);
128
- try {
129
- if (handler.id === "claude-code") {
130
- await writeClaudeCodeConfig(handler, token, mcpUrl, claudeCodeScope);
131
- }
132
- else {
133
- await handler.writeConfig(token, mcpUrl);
134
- }
135
- println(chalk.green("OK"));
136
- configured++;
137
- }
138
- catch (error) {
139
- println(chalk.red("FALHOU"));
140
- eprintln(chalk.gray(` ${error instanceof Error ? error.message : String(error)}`));
141
- }
363
+ function detectAllIDEs() {
364
+ return [
365
+ detectVSCode(),
366
+ detectVSCodeInsiders(),
367
+ detectCursor(),
368
+ detectJetBrains()
369
+ ];
370
+ }
371
+ function getInstalledIDEs() {
372
+ return detectAllIDEs().filter((ide) => ide.installed);
373
+ }
374
+ if (process.argv[1]?.endsWith("detect.js")) {
375
+ const installedIDEs = getInstalledIDEs();
376
+ if (installedIDEs.length === 0) {
377
+ process.stdout.write("No supported IDEs detected.\n");
378
+ process.stdout.write("\nSupported IDEs:\n");
379
+ process.stdout.write(" - Visual Studio Code\n");
380
+ process.stdout.write(" - Visual Studio Code - Insiders\n");
381
+ process.stdout.write(" - Cursor\n");
382
+ process.stdout.write(" - JetBrains IDEs (IntelliJ, WebStorm, PyCharm, etc.)\n");
383
+ } else {
384
+ process.stdout.write("Detected IDEs:\n");
385
+ for (const ide of installedIDEs) {
386
+ const versionStr = ide.version ? ` (${ide.version})` : "";
387
+ process.stdout.write(` \u2713 ${ide.name}${versionStr}
388
+ `);
389
+ process.stdout.write(` Config: ${ide.configPath || "N/A"}
390
+ `);
142
391
  }
143
- return configured;
392
+ }
393
+ process.stdout.write("\n--- JSON Output ---\n");
394
+ process.stdout.write(JSON.stringify(installedIDEs, null, 2) + "\n");
144
395
  }
145
- /**
146
- * Write Claude Code config with explicit scope (global or project-local).
147
- */
148
- async function writeClaudeCodeConfig(handler, token, mcpUrl, scope) {
149
- const { execFileSync } = await import("node:child_process");
150
- const { buildMcpRemoteArgs } = await import("./ide-handlers/base.js");
151
- const args = buildMcpRemoteArgs(token, mcpUrl, "claude-code");
152
- try {
153
- execFileSync("claude", ["mcp", "remove", "tostudy", "--scope", scope], { stdio: "ignore" });
396
+
397
+ // src/diagnose.ts
398
+ import { existsSync as existsSync3, readFileSync as readFileSync2, statSync } from "fs";
399
+ import { createServer } from "net";
400
+ import chalk from "chalk";
401
+ var DEFAULT_MCP_PORT = 3701;
402
+ function println2(message = "") {
403
+ process.stdout.write(`${message}
404
+ `);
405
+ }
406
+ async function isPortAvailable(port) {
407
+ return new Promise((resolve) => {
408
+ const server = createServer();
409
+ server.once("error", () => {
410
+ resolve(false);
411
+ });
412
+ server.once("listening", () => {
413
+ server.close();
414
+ resolve(true);
415
+ });
416
+ server.listen(port, "127.0.0.1");
417
+ });
418
+ }
419
+ function checkClaudeInstallation() {
420
+ if (!isClaudeInstalled()) {
421
+ return {
422
+ id: "claude-not-installed",
423
+ severity: "critical",
424
+ title: "Claude Code nao encontrado",
425
+ description: "O diretorio de configuracao do Claude Code nao foi encontrado no sistema.",
426
+ suggestion: "Instale o Claude Code em https://claude.ai/download",
427
+ autoFixable: false
428
+ };
429
+ }
430
+ return null;
431
+ }
432
+ function checkConfigFile() {
433
+ const configPath = getClaudeConfigPath();
434
+ if (!existsSync3(configPath)) {
435
+ return {
436
+ id: "config-missing",
437
+ severity: "warning",
438
+ title: "Arquivo de configuracao nao existe",
439
+ description: `O arquivo ${configPath} nao foi encontrado. Sera criado durante o setup.`,
440
+ suggestion: "Execute o comando de setup para criar a configuracao.",
441
+ autoFixable: true
442
+ };
443
+ }
444
+ try {
445
+ const stats = statSync(configPath);
446
+ if (!stats.isFile()) {
447
+ return {
448
+ id: "config-not-file",
449
+ severity: "critical",
450
+ title: "Configuracao nao e um arquivo",
451
+ description: `${configPath} existe mas nao e um arquivo valido.`,
452
+ suggestion: "Remova o item e execute o setup novamente.",
453
+ autoFixable: false
454
+ };
455
+ }
456
+ } catch {
457
+ return {
458
+ id: "config-unreadable",
459
+ severity: "critical",
460
+ title: "Arquivo de configuracao inacessivel",
461
+ description: `Nao foi possivel acessar ${configPath}. Verifique as permissoes.`,
462
+ suggestion: "Verifique as permissoes do arquivo ou recrie-o.",
463
+ autoFixable: false
464
+ };
465
+ }
466
+ return null;
467
+ }
468
+ function checkConfigJson() {
469
+ const configPath = getClaudeConfigPath();
470
+ if (!existsSync3(configPath)) {
471
+ return null;
472
+ }
473
+ try {
474
+ const content = readFileSync2(configPath, "utf-8");
475
+ JSON.parse(content);
476
+ } catch {
477
+ return {
478
+ id: "config-invalid-json",
479
+ severity: "critical",
480
+ title: "JSON de configuracao invalido",
481
+ description: "O arquivo de configuracao contem JSON invalido.",
482
+ suggestion: 'Execute "mcp-setup repair" para corrigir ou remova o arquivo e execute o setup novamente.',
483
+ autoFixable: true
484
+ };
485
+ }
486
+ return null;
487
+ }
488
+ function checkMcpConfiguration() {
489
+ if (!isTostudyMcpConfigured()) {
490
+ return {
491
+ id: "mcp-not-configured",
492
+ severity: "warning",
493
+ title: "ToStudy MCP nao configurado",
494
+ description: "O servidor MCP da ToStudy nao esta configurado no Claude Code.",
495
+ suggestion: "Execute o comando de setup para configurar o MCP.",
496
+ autoFixable: true
497
+ };
498
+ }
499
+ return null;
500
+ }
501
+ function checkApiKeyFormat() {
502
+ const mcpConfig = getTostudyMcpConfig();
503
+ if (!mcpConfig) {
504
+ return null;
505
+ }
506
+ const apiKey = extractApiKey(mcpConfig);
507
+ if (!apiKey) {
508
+ return {
509
+ id: "api-key-missing",
510
+ severity: "critical",
511
+ title: "API key nao configurada",
512
+ description: "A configuracao do MCP nao possui uma API key.",
513
+ suggestion: "Execute o setup novamente com uma API key valida.",
514
+ autoFixable: true
515
+ };
516
+ }
517
+ if (apiKey.length < 32) {
518
+ return {
519
+ id: "api-key-too-short",
520
+ severity: "warning",
521
+ title: "API key parece ser muito curta",
522
+ description: `A API key tem apenas ${apiKey.length} caracteres. API keys validas geralmente tem 32+ caracteres.`,
523
+ suggestion: "Verifique se copiou a API key completa de /student/settings/mcp.",
524
+ autoFixable: true
525
+ };
526
+ }
527
+ return null;
528
+ }
529
+ function checkServerUrl() {
530
+ const mcpConfig = getTostudyMcpConfig();
531
+ if (!mcpConfig) {
532
+ return null;
533
+ }
534
+ const url = extractMcpUrl(mcpConfig);
535
+ if (!url) {
536
+ return {
537
+ id: "server-url-missing",
538
+ severity: "critical",
539
+ title: "URL do servidor MCP nao configurada",
540
+ description: "A configuracao do MCP nao possui uma URL de servidor.",
541
+ suggestion: "Execute o setup novamente para configurar a URL correta.",
542
+ autoFixable: true
543
+ };
544
+ }
545
+ try {
546
+ const parsedUrl = new URL(url);
547
+ if (!["http:", "https:"].includes(parsedUrl.protocol)) {
548
+ return {
549
+ id: "server-url-invalid-protocol",
550
+ severity: "critical",
551
+ title: "Protocolo de URL invalido",
552
+ description: `O protocolo "${parsedUrl.protocol}" nao e suportado. Use http: ou https:.`,
553
+ suggestion: "Execute o setup novamente com a URL correta.",
554
+ autoFixable: true
555
+ };
154
556
  }
155
- catch {
156
- /* may not exist */
557
+ const validPaths = ["/mcp/sse", "/mcp"];
558
+ if (!validPaths.some((p) => parsedUrl.pathname.endsWith(p))) {
559
+ return {
560
+ id: "server-url-missing-sse-path",
561
+ severity: "warning",
562
+ title: "URL do servidor pode estar incorreta",
563
+ description: `A URL ${url} nao termina com /mcp como esperado.`,
564
+ suggestion: "Verifique se a URL esta correta ou execute o setup novamente.",
565
+ autoFixable: true
566
+ };
157
567
  }
158
- execFileSync("claude", ["mcp", "add", "tostudy", "--scope", scope, "--", "npx", ...args], {
159
- stdio: "inherit",
568
+ } catch {
569
+ return {
570
+ id: "server-url-invalid",
571
+ severity: "critical",
572
+ title: "URL do servidor invalida",
573
+ description: `A URL "${url}" nao e uma URL valida.`,
574
+ suggestion: "Execute o setup novamente com uma URL valida.",
575
+ autoFixable: true
576
+ };
577
+ }
578
+ return null;
579
+ }
580
+ async function checkPortConflict() {
581
+ const isAvailable = await isPortAvailable(DEFAULT_MCP_PORT);
582
+ if (!isAvailable) {
583
+ return {
584
+ id: "port-conflict",
585
+ severity: "warning",
586
+ title: `Porta ${DEFAULT_MCP_PORT} em uso`,
587
+ description: `A porta padrao do MCP server (${DEFAULT_MCP_PORT}) esta sendo usada por outro processo.`,
588
+ suggestion: "Verifique qual processo esta usando a porta ou use uma porta alternativa.",
589
+ autoFixable: false
590
+ };
591
+ }
592
+ return null;
593
+ }
594
+ async function checkServerConnectivity() {
595
+ const mcpConfig = getTostudyMcpConfig();
596
+ if (!mcpConfig) {
597
+ return null;
598
+ }
599
+ const mcpUrl = extractMcpUrl(mcpConfig);
600
+ const apiKey = extractApiKey(mcpConfig);
601
+ if (!mcpUrl) {
602
+ return null;
603
+ }
604
+ const baseUrl = mcpUrl.replace(/\/mcp(\/sse)?$/, "");
605
+ try {
606
+ const controller = new AbortController();
607
+ const timeout = setTimeout(() => controller.abort(), 5e3);
608
+ const response = await fetch(`${baseUrl}/api/mcp/heartbeat`, {
609
+ method: "POST",
610
+ headers: {
611
+ Authorization: apiKey ? `Bearer ${apiKey}` : "",
612
+ "Content-Type": "application/json"
613
+ },
614
+ body: JSON.stringify({ timestamp: (/* @__PURE__ */ new Date()).toISOString() }),
615
+ signal: controller.signal
160
616
  });
617
+ clearTimeout(timeout);
618
+ if (response.status === 401 || response.status === 403) {
619
+ return {
620
+ id: "auth-failed",
621
+ severity: "critical",
622
+ title: "Falha de autenticacao",
623
+ description: "O servidor MCP rejeitou a API key. Ela pode estar expirada ou invalida.",
624
+ suggestion: "Gere uma nova API key em /student/settings/mcp e execute o setup novamente.",
625
+ autoFixable: true
626
+ };
627
+ }
628
+ if (!response.ok && response.status !== 204) {
629
+ return {
630
+ id: "server-error",
631
+ severity: "warning",
632
+ title: `Servidor retornou erro ${response.status}`,
633
+ description: `O servidor MCP retornou um status de erro: ${response.status}.`,
634
+ suggestion: "O servidor pode estar temporariamente indisponivel. Tente novamente mais tarde.",
635
+ autoFixable: false
636
+ };
637
+ }
638
+ } catch (error) {
639
+ if (error instanceof Error && error.name === "AbortError") {
640
+ return {
641
+ id: "server-timeout",
642
+ severity: "warning",
643
+ title: "Timeout de conexao",
644
+ description: "O servidor MCP nao respondeu em 5 segundos.",
645
+ suggestion: "Verifique sua conexao de internet ou se o servidor esta online.",
646
+ autoFixable: false
647
+ };
648
+ }
649
+ return {
650
+ id: "server-unreachable",
651
+ severity: "warning",
652
+ title: "Servidor MCP inacessivel",
653
+ description: "Nao foi possivel conectar ao servidor MCP.",
654
+ suggestion: "Verifique sua conexao de internet ou se a URL do servidor esta correta.",
655
+ autoFixable: false
656
+ };
657
+ }
658
+ return null;
161
659
  }
162
- /**
163
- * Print success banner after IDE configuration.
164
- */
165
- function printSuccessBanner(configuredCount) {
166
- println();
167
- if (configuredCount > 0) {
168
- println(chalk.green(" ╔═══════════════════════════════════════════════╗"));
169
- println(chalk.green(" ║") +
170
- chalk.white.bold(` ${configuredCount} IDE${configuredCount > 1 ? "s" : ""} configurada${configuredCount > 1 ? "s" : ""} com sucesso!`.padEnd(42)) +
171
- chalk.green("║"));
172
- println(chalk.green(" ╚═══════════════════════════════════════════════╝"));
173
- }
174
- else {
175
- println(chalk.yellow(" Nenhuma IDE foi configurada."));
176
- println(chalk.gray(" Use: npx @tostudy-ai/mcp-setup install --ide <nome> --key <token>"));
177
- }
178
- println();
179
- println(chalk.white(" Próximos passos:"));
180
- println(chalk.gray(" 1.") + " Reinicie as IDEs configuradas");
181
- println(chalk.gray(" 2.") + " Peça ao assistente: " + chalk.cyan('"mostre meus cursos"'));
182
- println();
183
- println(chalk.gray(" ─────────────────────────────────────────"));
184
- println();
185
- println(chalk.white(" Prefere estudar pelo terminal?"));
186
- println(chalk.gray(" ") + chalk.cyan("npm install -g @tostudy-ai/cli"));
187
- println(chalk.gray(" ") +
188
- chalk.cyan("tostudy login") +
189
- chalk.gray(" ") +
190
- chalk.cyan("tostudy courses"));
191
- println();
192
- }
193
- /**
194
- * Main setup flow
195
- */
196
- async function setup(apiKey, platformUrl) {
197
- printBanner();
198
- const url = platformUrl || process.env.TOSTUDY_PLATFORM_URL || DEFAULT_PLATFORM_URL;
199
- // API key fallback: skip OAuth, use key directly
200
- if (apiKey || process.env.TOSTUDY_API_KEY) {
201
- const key = apiKey || process.env.TOSTUDY_API_KEY;
202
- println(chalk.gray(" Usando API key fornecida..."));
203
- process.stdout.write(chalk.gray(" Validando... "));
204
- const valid = await validateApiKey(key, url);
205
- if (!valid) {
206
- println(chalk.red("FALHOU"));
207
- println(chalk.red(" API key invalida ou expirada."));
208
- process.exit(1);
209
- }
210
- println(chalk.green("OK"));
211
- println();
212
- const configured = await configureIDEs(key, url);
213
- printSuccessBanner(configured);
214
- return;
215
- }
216
- // OAuth flow
217
- println(chalk.gray(" Abrindo browser para autenticacao...\n"));
218
- let code;
219
- try {
220
- const serverPromise = startCallbackServer(OAUTH_PORT);
221
- const authUrl = `${url}/api/cli/auth/authorize?port=${OAUTH_PORT}`;
222
- const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
223
- const openArgs = process.platform === "win32" ? ["", authUrl] : [authUrl];
224
- execFile(openCmd, openArgs, (err) => {
225
- if (err) {
226
- println(chalk.yellow(" Não foi possível abrir o browser automaticamente."));
227
- println(chalk.gray(` Abra manualmente: ${authUrl}\n`));
228
- }
229
- });
230
- const result = await serverPromise;
231
- code = result.code;
232
- }
233
- catch (err) {
234
- const msg = err instanceof Error ? err.message : String(err);
235
- eprintln(chalk.red(` Erro: ${msg}`));
236
- process.exit(1);
237
- }
238
- // Exchange code for JWT
239
- process.stdout.write(chalk.gray(" Autenticando... "));
240
- let token;
241
- let userName;
242
- try {
243
- const result = await exchangeCodeForToken(code, url, "mcp");
244
- token = result.token;
245
- userName = result.userName;
246
- println(chalk.green("OK"));
247
- println(chalk.green(` Logado como ${userName}\n`));
248
- }
249
- catch (err) {
250
- println(chalk.red("FALHOU"));
251
- eprintln(chalk.red(` ${err instanceof Error ? err.message : String(err)}`));
252
- process.exit(1);
253
- }
254
- // Configure all detected IDEs
255
- println(chalk.gray(" Detectando IDEs...\n"));
256
- const configured = await configureIDEs(token, url);
257
- printSuccessBanner(configured);
258
- }
259
- /**
260
- * Uninstall flow
261
- */
262
- async function uninstall() {
263
- printBanner();
264
- if (!isTostudyMcpConfigured()) {
265
- println(chalk.yellow("ToStudy MCP nao esta configurado."));
266
- process.exit(0);
660
+ function checkIDEInstallation() {
661
+ const installedIDEs = getInstalledIDEs();
662
+ if (installedIDEs.length === 0) {
663
+ return {
664
+ id: "no-ide-detected",
665
+ severity: "info",
666
+ title: "Nenhuma IDE suportada detectada",
667
+ description: "Nao foi encontrada nenhuma IDE compativel (VS Code, Cursor, JetBrains).",
668
+ suggestion: "Instale uma IDE suportada para usar a integracao completa.",
669
+ autoFixable: false
670
+ };
671
+ }
672
+ return null;
673
+ }
674
+ function checkDuplicateServers() {
675
+ const config = readClaudeConfig();
676
+ if (!config.mcpServers) {
677
+ return null;
678
+ }
679
+ const servers = Object.entries(config.mcpServers);
680
+ const anaServers = servers.filter(
681
+ ([key, server]) => key.toLowerCase().includes("ana") || key.toLowerCase().includes("catalyst") || server.url && server.url.includes("tostudy.com")
682
+ );
683
+ if (anaServers.length > 1) {
684
+ return {
685
+ id: "duplicate-servers",
686
+ severity: "warning",
687
+ title: "Multiplos servidores ToStudy configurados",
688
+ description: `Foram encontrados ${anaServers.length} servidores relacionados a ToStudy: ${anaServers.map(([k]) => k).join(", ")}.`,
689
+ suggestion: 'Execute "mcp-setup repair" para remover duplicatas ou limpe manualmente.',
690
+ autoFixable: true
691
+ };
692
+ }
693
+ return null;
694
+ }
695
+ async function runDiagnostics() {
696
+ const issues = [];
697
+ const syncChecks = [
698
+ checkClaudeInstallation(),
699
+ checkConfigFile(),
700
+ checkConfigJson(),
701
+ checkMcpConfiguration(),
702
+ checkApiKeyFormat(),
703
+ checkServerUrl(),
704
+ checkIDEInstallation(),
705
+ checkDuplicateServers()
706
+ ];
707
+ for (const issue of syncChecks) {
708
+ if (issue) {
709
+ issues.push(issue);
710
+ }
711
+ }
712
+ const hasConfigIssues = issues.some(
713
+ (issue) => ["config-invalid-json", "claude-not-installed"].includes(issue.id)
714
+ );
715
+ if (!hasConfigIssues) {
716
+ const asyncChecks = await Promise.all([checkPortConflict(), checkServerConnectivity()]);
717
+ for (const issue of asyncChecks) {
718
+ if (issue) {
719
+ issues.push(issue);
720
+ }
267
721
  }
268
- const shouldUninstall = await confirm("Remover configuracao do ToStudy MCP?", false);
269
- if (!shouldUninstall) {
270
- println(chalk.gray("Operacao cancelada."));
271
- process.exit(0);
722
+ }
723
+ const severityOrder = {
724
+ critical: 0,
725
+ warning: 1,
726
+ info: 2
727
+ };
728
+ issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
729
+ const hasCritical = issues.some((issue) => issue.severity === "critical");
730
+ return {
731
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
732
+ claudeInstalled: isClaudeInstalled(),
733
+ mcpConfigured: isTostudyMcpConfigured(),
734
+ issues,
735
+ passed: !hasCritical
736
+ };
737
+ }
738
+ function getSeverityIcon(severity) {
739
+ switch (severity) {
740
+ case "critical":
741
+ return chalk.red("\u25CF");
742
+ case "warning":
743
+ return chalk.yellow("\u25CF");
744
+ case "info":
745
+ return chalk.blue("\u25CF");
746
+ }
747
+ }
748
+ function getSeverityLabel(severity) {
749
+ switch (severity) {
750
+ case "critical":
751
+ return chalk.red("CRITICO");
752
+ case "warning":
753
+ return chalk.yellow("AVISO");
754
+ case "info":
755
+ return chalk.blue("INFO");
756
+ }
757
+ }
758
+ function printDiagnosticReport(report) {
759
+ println2();
760
+ println2(chalk.cyan(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
761
+ println2(
762
+ chalk.cyan(" \u2551") + chalk.white.bold(" ToStudy MCP Diagnostics ") + chalk.cyan("\u2551")
763
+ );
764
+ println2(chalk.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
765
+ println2();
766
+ println2(chalk.gray("Status:"));
767
+ println2(
768
+ ` Claude Code: ${report.claudeInstalled ? chalk.green("\u2713 Instalado") : chalk.red("\u2717 Nao encontrado")}`
769
+ );
770
+ println2(
771
+ ` MCP ToStudy: ${report.mcpConfigured ? chalk.green("\u2713 Configurado") : chalk.yellow("\u25CB Nao configurado")}`
772
+ );
773
+ println2();
774
+ if (report.issues.length === 0) {
775
+ println2(chalk.green.bold("\u2713 Nenhum problema encontrado!"));
776
+ println2();
777
+ println2(chalk.gray("Tudo parece estar funcionando corretamente."));
778
+ println2();
779
+ return;
780
+ }
781
+ const criticalCount = report.issues.filter((i) => i.severity === "critical").length;
782
+ const warningCount = report.issues.filter((i) => i.severity === "warning").length;
783
+ const infoCount = report.issues.filter((i) => i.severity === "info").length;
784
+ println2(chalk.white.bold("Problemas encontrados:"));
785
+ if (criticalCount > 0) println2(` ${chalk.red("\u25CF")} ${criticalCount} critico(s)`);
786
+ if (warningCount > 0) println2(` ${chalk.yellow("\u25CF")} ${warningCount} aviso(s)`);
787
+ if (infoCount > 0) println2(` ${chalk.blue("\u25CF")} ${infoCount} informativo(s)`);
788
+ println2();
789
+ for (const issue of report.issues) {
790
+ println2(
791
+ `${getSeverityIcon(issue.severity)} ${getSeverityLabel(issue.severity)}: ${chalk.white(issue.title)}`
792
+ );
793
+ println2(chalk.gray(` ${issue.description}`));
794
+ println2(chalk.cyan(` \u2192 ${issue.suggestion}`));
795
+ if (issue.autoFixable) {
796
+ println2(chalk.green(` \u2713 Corrigivel automaticamente`));
272
797
  }
273
- process.stdout.write(chalk.gray("Removendo configuracao... "));
798
+ println2();
799
+ }
800
+ if (report.passed) {
801
+ println2(chalk.yellow("\u26A0 Alguns avisos encontrados, mas o sistema deve funcionar."));
802
+ } else {
803
+ println2(chalk.red("\u2717 Problemas criticos precisam ser resolvidos."));
804
+ println2(chalk.gray(' Execute "npx @tostudy-ai/mcp-setup" para corrigir a configuracao.'));
805
+ }
806
+ println2();
807
+ }
808
+ function exportDiagnosticReportJson(report) {
809
+ return JSON.stringify(report, null, 2);
810
+ }
811
+ if (process.argv[1]?.endsWith("diagnose.js")) {
812
+ (async () => {
274
813
  try {
275
- removeTostudyMcpServer();
276
- println(chalk.green("OK"));
277
- println();
278
- println(chalk.green("Configuracao removida com sucesso."));
279
- println(chalk.gray("Reinicie o Claude Code para aplicar as mudancas."));
280
- println();
281
- }
282
- catch (error) {
283
- println(chalk.red("FALHOU"));
284
- eprintln(error instanceof Error ? error.message : String(error));
285
- process.exit(1);
286
- }
287
- }
288
- /**
289
- * Print wizard step header
290
- */
291
- function printStep(step, total, title) {
292
- println();
293
- println(chalk.cyan(`━━━ Passo ${step}/${total}: ${title} ━━━`));
294
- println();
814
+ const report = await runDiagnostics();
815
+ if (process.argv.includes("--json")) {
816
+ println2(exportDiagnosticReportJson(report));
817
+ } else {
818
+ printDiagnosticReport(report);
819
+ }
820
+ process.exit(report.passed ? 0 : 1);
821
+ } catch (error) {
822
+ process.stderr.write(chalk.red("Erro ao executar diagnostico:\n"));
823
+ process.stderr.write((error instanceof Error ? error.message : String(error)) + "\n");
824
+ process.exit(1);
825
+ }
826
+ })();
295
827
  }
296
- /**
297
- * Print IDE detection results
298
- */
299
- function printIDEDetection(installedIDEs) {
300
- if (installedIDEs.length === 0) {
301
- println(chalk.yellow(" Nenhuma IDE suportada detectada."));
302
- println(chalk.gray(" O setup continuara para Claude Code."));
303
- }
304
- else {
305
- println(chalk.green(" IDEs detectadas:"));
306
- for (const ide of installedIDEs) {
307
- const version = ide.version ? chalk.gray(` (${ide.version})`) : "";
308
- println(` ${chalk.green("✓")} ${ide.name}${version}`);
309
- }
828
+
829
+ // src/repair.ts
830
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, copyFileSync as copyFileSync2 } from "fs";
831
+ import { dirname as dirname2 } from "path";
832
+ import chalk2 from "chalk";
833
+ var DEFAULT_PLATFORM_URL = "https://tostudy.ai";
834
+ function println3(message = "") {
835
+ process.stdout.write(`${message}
836
+ `);
837
+ }
838
+ function repairMissingConfig() {
839
+ const configPath = getClaudeConfigPath();
840
+ const configDir = dirname2(configPath);
841
+ try {
842
+ if (!existsSync4(configDir)) {
843
+ mkdirSync2(configDir, { recursive: true });
310
844
  }
845
+ const initialConfig = {
846
+ mcpServers: {}
847
+ };
848
+ writeFileSync2(configPath, JSON.stringify(initialConfig, null, 2), "utf-8");
849
+ return {
850
+ issueId: "config-missing",
851
+ success: true,
852
+ message: "Arquivo de configuracao criado com sucesso.",
853
+ action: `Criado: ${configPath}`
854
+ };
855
+ } catch (error) {
856
+ return {
857
+ issueId: "config-missing",
858
+ success: false,
859
+ message: `Falha ao criar arquivo de configuracao: ${error instanceof Error ? error.message : String(error)}`
860
+ };
861
+ }
311
862
  }
312
- /**
313
- * Interactive wizard flow
314
- * Guides user through complete MCP setup with step-by-step instructions
315
- */
316
- async function wizard(options) {
317
- const TOTAL_STEPS = 4;
318
- // Print wizard banner
319
- println();
320
- println(chalk.cyan(" ╔═══════════════════════════════════════════════╗"));
321
- println(chalk.cyan("") +
322
- chalk.white.bold(" ToStudy MCP Setup Wizard ") +
323
- chalk.cyan(""));
324
- println(chalk.cyan(" ║") + chalk.gray(" Configuracao guiada passo a passo ") + chalk.cyan("║"));
325
- println(chalk.cyan(" ╚═══════════════════════════════════════════════╝"));
326
- println();
327
- println(chalk.gray(" Este assistente vai configurar o Claude Code para"));
328
- println(chalk.gray(" conectar ao servidor MCP da plataforma ToStudy."));
329
- println();
330
- // ━━━ Step 1: Environment Detection ━━━
331
- printStep(1, TOTAL_STEPS, "Detectando ambiente");
332
- // Check Claude Code
333
- process.stdout.write(" Verificando Claude Code... ");
334
- if (!isClaudeInstalled()) {
335
- println(chalk.red("NAO ENCONTRADO"));
336
- println();
337
- println(chalk.red(" Claude Code nao esta instalado."));
338
- println();
339
- println(" Por favor, instale primeiro:");
340
- println(chalk.cyan(" https://claude.ai/download"));
341
- println();
342
- process.exit(1);
343
- }
344
- println(chalk.green("OK"));
345
- const configPath = getClaudeConfigPath();
346
- println(chalk.gray(` Config: ${configPath}`));
347
- // Check for other IDEs
348
- process.stdout.write(" Detectando IDEs... ");
349
- const installedIDEs = getInstalledIDEs();
350
- println(chalk.green("OK"));
351
- printIDEDetection(installedIDEs);
352
- // Check existing configuration
353
- process.stdout.write(" Verificando configuracao atual... ");
354
- const alreadyConfigured = isTostudyMcpConfigured();
355
- if (alreadyConfigured) {
356
- println(chalk.yellow("JA CONFIGURADO"));
357
- println();
358
- const overwrite = await confirm(" Deseja reconfigurar?", false);
359
- if (!overwrite) {
360
- println();
361
- println(chalk.gray(" Operacao cancelada."));
362
- process.exit(0);
863
+ function repairInvalidJson() {
864
+ const configPath = getClaudeConfigPath();
865
+ try {
866
+ if (existsSync4(configPath)) {
867
+ const backupPath = `${configPath}.corrupted.${Date.now()}`;
868
+ copyFileSync2(configPath, backupPath);
869
+ const freshConfig = {
870
+ mcpServers: {}
871
+ };
872
+ writeFileSync2(configPath, JSON.stringify(freshConfig, null, 2), "utf-8");
873
+ return {
874
+ issueId: "config-invalid-json",
875
+ success: true,
876
+ message: "Arquivo de configuracao recriado. Backup salvo.",
877
+ action: `Backup: ${backupPath}`
878
+ };
879
+ }
880
+ return {
881
+ issueId: "config-invalid-json",
882
+ success: false,
883
+ message: "Arquivo de configuracao nao existe."
884
+ };
885
+ } catch (error) {
886
+ return {
887
+ issueId: "config-invalid-json",
888
+ success: false,
889
+ message: `Falha ao reparar JSON: ${error instanceof Error ? error.message : String(error)}`
890
+ };
891
+ }
892
+ }
893
+ function repairMcpNotConfigured(apiKey, platformUrl = DEFAULT_PLATFORM_URL) {
894
+ try {
895
+ addTostudyMcpServer(apiKey, platformUrl);
896
+ return {
897
+ issueId: "mcp-not-configured",
898
+ success: true,
899
+ message: "Servidor MCP da ToStudy configurado com sucesso.",
900
+ action: "Servidor tostudy adicionado"
901
+ };
902
+ } catch (error) {
903
+ return {
904
+ issueId: "mcp-not-configured",
905
+ success: false,
906
+ message: `Falha ao configurar MCP: ${error instanceof Error ? error.message : String(error)}`
907
+ };
908
+ }
909
+ }
910
+ function repairApiKeyMissing(apiKey, platformUrl = DEFAULT_PLATFORM_URL) {
911
+ try {
912
+ addTostudyMcpServer(apiKey, platformUrl);
913
+ return {
914
+ issueId: "api-key-missing",
915
+ success: true,
916
+ message: "API key configurada com sucesso.",
917
+ action: "Header Authorization adicionado"
918
+ };
919
+ } catch (error) {
920
+ return {
921
+ issueId: "api-key-missing",
922
+ success: false,
923
+ message: `Falha ao configurar API key: ${error instanceof Error ? error.message : String(error)}`
924
+ };
925
+ }
926
+ }
927
+ function repairApiKeyFormat() {
928
+ const config = readClaudeConfig();
929
+ const mcpConfig = config.mcpServers?.["tostudy"];
930
+ if (!mcpConfig) {
931
+ return {
932
+ issueId: "api-key-invalid-format",
933
+ success: false,
934
+ message: "Servidor MCP nao esta configurado."
935
+ };
936
+ }
937
+ try {
938
+ if (mcpConfig.headers) {
939
+ const authHeader = mcpConfig.headers["Authorization"] || "";
940
+ if (authHeader && !authHeader.startsWith("Bearer ")) {
941
+ mcpConfig.headers["Authorization"] = `Bearer ${authHeader}`;
942
+ writeClaudeConfig(config);
943
+ return {
944
+ issueId: "api-key-invalid-format",
945
+ success: true,
946
+ message: "Formato de API key corrigido.",
947
+ action: 'Prefixo "Bearer " adicionado'
948
+ };
949
+ }
950
+ }
951
+ if (mcpConfig.args) {
952
+ for (let i = 0; i < mcpConfig.args.length; i++) {
953
+ if (mcpConfig.args[i] === "--header" && mcpConfig.args[i + 1]?.startsWith("Authorization:")) {
954
+ const value = mcpConfig.args[i + 1].replace("Authorization:", "");
955
+ if (value && !value.startsWith("Bearer ")) {
956
+ mcpConfig.args[i + 1] = `Authorization:Bearer ${value}`;
957
+ writeClaudeConfig(config);
958
+ return {
959
+ issueId: "api-key-invalid-format",
960
+ success: true,
961
+ message: "Formato de API key corrigido.",
962
+ action: 'Prefixo "Bearer " adicionado'
963
+ };
964
+ }
363
965
  }
966
+ }
364
967
  }
365
- else {
366
- println(chalk.gray("Nao configurado"));
968
+ return {
969
+ issueId: "api-key-invalid-format",
970
+ success: false,
971
+ message: "API key nao encontrada para corrigir."
972
+ };
973
+ } catch (error) {
974
+ return {
975
+ issueId: "api-key-invalid-format",
976
+ success: false,
977
+ message: `Falha ao corrigir formato: ${error instanceof Error ? error.message : String(error)}`
978
+ };
979
+ }
980
+ }
981
+ function repairServerUrl(platformUrl = DEFAULT_PLATFORM_URL) {
982
+ const config = readClaudeConfig();
983
+ const mcpConfig = config.mcpServers?.["tostudy"];
984
+ if (!mcpConfig) {
985
+ return {
986
+ issueId: "server-url-missing",
987
+ success: false,
988
+ message: "Servidor MCP nao esta configurado."
989
+ };
990
+ }
991
+ try {
992
+ const apiKey = extractApiKey(mcpConfig);
993
+ if (!apiKey) {
994
+ return {
995
+ issueId: "server-url-missing",
996
+ success: false,
997
+ message: "API key nao encontrada para reconstruir configuracao."
998
+ };
367
999
  }
368
- // ━━━ Step 2: API Key Configuration ━━━
369
- printStep(2, TOTAL_STEPS, "Configurando API Key");
370
- println(chalk.gray(" A API key conecta o Claude Code a sua conta na plataforma."));
371
- println();
372
- let apiKey = options.apiKey || process.env.TOSTUDY_API_KEY;
1000
+ addTostudyMcpServer(apiKey, platformUrl);
1001
+ return {
1002
+ issueId: "server-url-missing",
1003
+ success: true,
1004
+ message: "URL do servidor configurada.",
1005
+ action: `URL: ${platformUrl}/mcp`
1006
+ };
1007
+ } catch (error) {
1008
+ return {
1009
+ issueId: "server-url-missing",
1010
+ success: false,
1011
+ message: `Falha ao configurar URL: ${error instanceof Error ? error.message : String(error)}`
1012
+ };
1013
+ }
1014
+ }
1015
+ function repairServerUrlProtocol(platformUrl = DEFAULT_PLATFORM_URL) {
1016
+ const config = readClaudeConfig();
1017
+ const mcpConfig = config.mcpServers?.["tostudy"];
1018
+ if (!mcpConfig) {
1019
+ return {
1020
+ issueId: "server-url-invalid-protocol",
1021
+ success: false,
1022
+ message: "Servidor MCP nao esta configurado."
1023
+ };
1024
+ }
1025
+ const url = extractMcpUrl(mcpConfig);
1026
+ if (!url) {
1027
+ return {
1028
+ issueId: "server-url-invalid-protocol",
1029
+ success: false,
1030
+ message: "URL do servidor nao configurada."
1031
+ };
1032
+ }
1033
+ try {
1034
+ const fixedUrl = url.replace(/^[a-z]+:/, "https:");
1035
+ const basePlatformUrl = fixedUrl.replace(/\/mcp(\/sse)?$/, "");
1036
+ const apiKey = extractApiKey(mcpConfig);
373
1037
  if (!apiKey) {
374
- println(" Acesse sua API key em:");
375
- println(chalk.cyan(` ${options.url || DEFAULT_PLATFORM_URL}/student/settings/mcp`));
376
- println();
377
- apiKey = await promptApiKey();
378
- }
379
- else {
380
- println(chalk.green(" ✓ API key fornecida via parametro ou ambiente"));
381
- }
382
- const platformUrl = options.url || process.env.TOSTUDY_PLATFORM_URL || DEFAULT_PLATFORM_URL;
383
- // Validate API key
384
- println();
385
- process.stdout.write(chalk.gray(" Validando API key... "));
386
- const valid = await validateApiKey(apiKey, platformUrl);
387
- if (!valid) {
388
- println(chalk.red("FALHOU"));
389
- println();
390
- println(chalk.red(" API key invalida ou expirada."));
391
- println();
392
- println(" Para gerar uma nova API key:");
393
- println(chalk.cyan(` ${platformUrl}/student/settings/mcp`));
394
- println();
395
- process.exit(1);
396
- }
397
- println(chalk.green("OK"));
398
- // ━━━ Step 3: Save Configuration ━━━
399
- printStep(3, TOTAL_STEPS, "Salvando configuracao");
400
- process.stdout.write(" Configurando Claude Code... ");
401
- try {
402
- addTostudyMcpServer(apiKey, platformUrl);
403
- println(chalk.green("OK"));
404
- }
405
- catch (error) {
406
- println(chalk.red("FALHOU"));
407
- eprintln();
408
- eprintln(chalk.red(" Erro ao salvar configuracao:"));
409
- eprintln(" " + (error instanceof Error ? error.message : String(error)));
410
- process.exit(1);
411
- }
412
- // ━━━ Step 4: Diagnostics & Verification ━━━
413
- printStep(4, TOTAL_STEPS, "Verificacao final");
414
- if (!options.skipDiagnostics) {
415
- process.stdout.write(" Executando diagnostico... ");
416
- const report = await runDiagnostics();
417
- println(chalk.green("OK"));
418
- println();
419
- // Filter out expected issues that we just fixed
420
- const remainingIssues = report.issues.filter((issue) => !["mcp-not-configured", "config-missing"].includes(issue.id));
421
- if (remainingIssues.length > 0) {
422
- println(chalk.yellow(" Avisos encontrados:"));
423
- for (const issue of remainingIssues) {
424
- const icon = issue.severity === "critical"
425
- ? chalk.red("●")
426
- : issue.severity === "warning"
427
- ? chalk.yellow("●")
428
- : chalk.blue("●");
429
- println(` ${icon} ${issue.title}`);
430
- }
431
- println();
432
- // Auto-repair if enabled
433
- if (options.autoRepair) {
434
- process.stdout.write(" Tentando reparar automaticamente... ");
435
- const repairReport = repairAllIssues(report, apiKey, platformUrl);
436
- if (repairReport.repairsSucceeded > 0) {
437
- println(chalk.green(`${repairReport.repairsSucceeded} corrigido(s)`));
438
- }
439
- else {
440
- println(chalk.yellow("Nenhum reparo aplicado"));
441
- }
442
- }
443
- else {
444
- println(chalk.gray(' Dica: Execute "npx @tostudy-ai/mcp-setup diagnose" para mais detalhes.'));
445
- }
446
- }
447
- else {
448
- println(chalk.green(" ✓ Nenhum problema encontrado!"));
449
- }
1038
+ return {
1039
+ issueId: "server-url-invalid-protocol",
1040
+ success: false,
1041
+ message: "API key nao encontrada para reconstruir configuracao."
1042
+ };
1043
+ }
1044
+ addTostudyMcpServer(apiKey, basePlatformUrl);
1045
+ return {
1046
+ issueId: "server-url-invalid-protocol",
1047
+ success: true,
1048
+ message: "Protocolo da URL corrigido para HTTPS.",
1049
+ action: `URL: ${basePlatformUrl}/mcp`
1050
+ };
1051
+ } catch (error) {
1052
+ return {
1053
+ issueId: "server-url-invalid-protocol",
1054
+ success: false,
1055
+ message: `Falha ao corrigir protocolo: ${error instanceof Error ? error.message : String(error)}`
1056
+ };
1057
+ }
1058
+ }
1059
+ function repairServerUrlPath() {
1060
+ const config = readClaudeConfig();
1061
+ const mcpConfig = config.mcpServers?.["tostudy"];
1062
+ if (!mcpConfig) {
1063
+ return {
1064
+ issueId: "server-url-missing-sse-path",
1065
+ success: false,
1066
+ message: "Servidor MCP nao esta configurado."
1067
+ };
1068
+ }
1069
+ const url = extractMcpUrl(mcpConfig);
1070
+ if (!url) {
1071
+ return {
1072
+ issueId: "server-url-missing-sse-path",
1073
+ success: false,
1074
+ message: "URL do servidor nao configurada."
1075
+ };
1076
+ }
1077
+ try {
1078
+ const parsedUrl = new URL(url);
1079
+ parsedUrl.pathname = parsedUrl.pathname.replace(/\/(mcp(\/sse)?)?$/, "");
1080
+ const basePlatformUrl = parsedUrl.toString().replace(/\/$/, "");
1081
+ const apiKey = extractApiKey(mcpConfig);
1082
+ if (!apiKey) {
1083
+ return {
1084
+ issueId: "server-url-missing-sse-path",
1085
+ success: false,
1086
+ message: "API key nao encontrada para reconstruir configuracao."
1087
+ };
450
1088
  }
451
- else {
452
- println(chalk.gray(" Diagnostico ignorado (--skip-diagnostics)"));
453
- }
454
- // ━━━ Success ━━━
455
- println();
456
- println(chalk.green(" ╔═══════════════════════════════════════════════╗"));
457
- println(chalk.green(" ║") +
458
- chalk.white.bold(" Configuracao concluida com sucesso! ") +
459
- chalk.green("║"));
460
- println(chalk.green(" ╚═══════════════════════════════════════════════╝"));
461
- println();
462
- println(chalk.white(" Proximos passos:"));
463
- println();
464
- println(chalk.gray(" 1.") + " Reinicie o Claude Code");
465
- println(chalk.gray(" 2.") + " O servidor MCP iniciara automaticamente");
466
- println(chalk.gray(" 3.") + " Use " + chalk.cyan("/courses") + " para ver seus cursos");
467
- println();
468
- println(chalk.gray(" Comandos uteis:"));
469
- println(chalk.gray(" ") +
470
- chalk.cyan("npx @tostudy-ai/mcp-setup diagnose") +
471
- chalk.gray(" - Verificar problemas"));
472
- println(chalk.gray(" ") +
473
- chalk.cyan("npx @tostudy-ai/mcp-setup repair") +
474
- chalk.gray(" - Reparar automaticamente"));
475
- println();
476
- println(chalk.gray(" ─────────────────────────────────────────"));
477
- println();
478
- println(chalk.white(" Prefere estudar pelo terminal?"));
479
- println(chalk.gray(" ") + chalk.cyan("npm install -g @tostudy-ai/cli"));
480
- println(chalk.gray(" ") +
481
- chalk.cyan("tostudy login") +
482
- chalk.gray(" ") +
483
- chalk.cyan("tostudy courses"));
484
- println();
485
- }
486
- // CLI setup
487
- program
488
- .name("tostudy-mcp-setup")
489
- .description("Configura o Claude Code para usar o ToStudy MCP server")
490
- .version(VERSION);
491
- // Default command (setup)
492
- program
493
- .option("-k, --api-key <key>", "API key (fallback para ambientes sem browser)")
494
- .option("-u, --url <url>", "URL da plataforma (default: https://tostudy.ai)")
495
- .option("--uninstall", "Remove a configuracao do ToStudy MCP")
496
- .action(async (options) => {
1089
+ addTostudyMcpServer(apiKey, basePlatformUrl);
1090
+ return {
1091
+ issueId: "server-url-missing-sse-path",
1092
+ success: true,
1093
+ message: "Path /mcp corrigido na URL.",
1094
+ action: `URL: ${basePlatformUrl}/mcp`
1095
+ };
1096
+ } catch (error) {
1097
+ return {
1098
+ issueId: "server-url-missing-sse-path",
1099
+ success: false,
1100
+ message: `Falha ao corrigir path: ${error instanceof Error ? error.message : String(error)}`
1101
+ };
1102
+ }
1103
+ }
1104
+ function repairDuplicateServers() {
1105
+ const config = readClaudeConfig();
1106
+ if (!config.mcpServers) {
1107
+ return {
1108
+ issueId: "duplicate-servers",
1109
+ success: false,
1110
+ message: "Nenhum servidor MCP configurado."
1111
+ };
1112
+ }
1113
+ try {
1114
+ const servers = Object.entries(config.mcpServers);
1115
+ const anaServers = servers.filter(
1116
+ ([key, server]) => key.toLowerCase().includes("ana") || key.toLowerCase().includes("catalyst") || server.url && server.url.includes("tostudy.com")
1117
+ );
1118
+ if (anaServers.length <= 1) {
1119
+ return {
1120
+ issueId: "duplicate-servers",
1121
+ success: true,
1122
+ message: "Nenhuma duplicata encontrada."
1123
+ };
1124
+ }
1125
+ const primaryKey = anaServers.find(([key]) => key === "tostudy")?.[0] || anaServers[0][0];
1126
+ const removedKeys = [];
1127
+ for (const [key] of anaServers) {
1128
+ if (key !== primaryKey) {
1129
+ delete config.mcpServers[key];
1130
+ removedKeys.push(key);
1131
+ }
1132
+ }
1133
+ writeClaudeConfig(config);
1134
+ return {
1135
+ issueId: "duplicate-servers",
1136
+ success: true,
1137
+ message: `${removedKeys.length} servidor(es) duplicado(s) removido(s).`,
1138
+ action: `Removidos: ${removedKeys.join(", ")}`
1139
+ };
1140
+ } catch (error) {
1141
+ return {
1142
+ issueId: "duplicate-servers",
1143
+ success: false,
1144
+ message: `Falha ao remover duplicatas: ${error instanceof Error ? error.message : String(error)}`
1145
+ };
1146
+ }
1147
+ }
1148
+ var REPAIR_FUNCTIONS = {
1149
+ "config-missing": () => repairMissingConfig(),
1150
+ "config-invalid-json": () => repairInvalidJson(),
1151
+ "mcp-not-configured": (apiKey, platformUrl) => {
1152
+ if (!apiKey) {
1153
+ return {
1154
+ issueId: "mcp-not-configured",
1155
+ success: false,
1156
+ message: "API key necessaria para configurar o MCP."
1157
+ };
1158
+ }
1159
+ return repairMcpNotConfigured(apiKey, platformUrl);
1160
+ },
1161
+ "api-key-missing": (apiKey, platformUrl) => {
1162
+ if (!apiKey) {
1163
+ return {
1164
+ issueId: "api-key-missing",
1165
+ success: false,
1166
+ message: "Nova API key necessaria."
1167
+ };
1168
+ }
1169
+ return repairApiKeyMissing(apiKey, platformUrl);
1170
+ },
1171
+ "api-key-invalid-format": () => repairApiKeyFormat(),
1172
+ "api-key-too-short": (apiKey, platformUrl) => {
1173
+ if (!apiKey) {
1174
+ return {
1175
+ issueId: "api-key-too-short",
1176
+ success: false,
1177
+ message: "Nova API key necessaria."
1178
+ };
1179
+ }
1180
+ return repairApiKeyMissing(apiKey, platformUrl);
1181
+ },
1182
+ "server-url-missing": (_, platformUrl) => repairServerUrl(platformUrl),
1183
+ "server-url-invalid": (_, platformUrl) => repairServerUrl(platformUrl),
1184
+ "server-url-invalid-protocol": () => repairServerUrlProtocol(),
1185
+ "server-url-missing-sse-path": () => repairServerUrlPath(),
1186
+ "auth-failed": (apiKey, platformUrl) => {
1187
+ if (!apiKey) {
1188
+ return {
1189
+ issueId: "auth-failed",
1190
+ success: false,
1191
+ message: "Nova API key necessaria. Gere em /student/settings/mcp."
1192
+ };
1193
+ }
1194
+ return repairMcpNotConfigured(apiKey, platformUrl);
1195
+ },
1196
+ "duplicate-servers": () => repairDuplicateServers()
1197
+ };
1198
+ var ISSUES_REQUIRING_API_KEY = [
1199
+ "mcp-not-configured",
1200
+ "api-key-missing",
1201
+ "api-key-too-short",
1202
+ "auth-failed"
1203
+ ];
1204
+ function issueRequiresUserInput(issueId) {
1205
+ return ISSUES_REQUIRING_API_KEY.includes(issueId);
1206
+ }
1207
+ function repairIssue(issue, apiKey, platformUrl) {
1208
+ const repairFn = REPAIR_FUNCTIONS[issue.id];
1209
+ if (!repairFn) {
1210
+ return {
1211
+ issueId: issue.id,
1212
+ success: false,
1213
+ message: `Sem funcao de reparo disponivel para: ${issue.id}`
1214
+ };
1215
+ }
1216
+ return repairFn(apiKey, platformUrl);
1217
+ }
1218
+ function repairAllIssues(report, apiKey, platformUrl = DEFAULT_PLATFORM_URL) {
1219
+ const results = [];
1220
+ const requiresUserInput = [];
1221
+ let succeeded = 0;
1222
+ let failed = 0;
1223
+ const fixableIssues = report.issues.filter((issue) => issue.autoFixable);
1224
+ for (const issue of fixableIssues) {
1225
+ if (issueRequiresUserInput(issue.id) && !apiKey) {
1226
+ requiresUserInput.push(issue.id);
1227
+ results.push({
1228
+ issueId: issue.id,
1229
+ success: false,
1230
+ message: "Requer API key para reparar."
1231
+ });
1232
+ failed++;
1233
+ continue;
1234
+ }
1235
+ const result = repairIssue(issue, apiKey, platformUrl);
1236
+ results.push(result);
1237
+ if (result.success) {
1238
+ succeeded++;
1239
+ } else {
1240
+ failed++;
1241
+ }
1242
+ }
1243
+ return {
1244
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1245
+ repairsAttempted: fixableIssues.length,
1246
+ repairsSucceeded: succeeded,
1247
+ repairsFailed: failed,
1248
+ results,
1249
+ requiresUserInput
1250
+ };
1251
+ }
1252
+ async function diagnoseAndRepair(apiKey, platformUrl = DEFAULT_PLATFORM_URL) {
1253
+ const diagnostic = await runDiagnostics();
1254
+ const repair = repairAllIssues(diagnostic, apiKey, platformUrl);
1255
+ return { diagnostic, repair };
1256
+ }
1257
+ function printRepairReport(report) {
1258
+ println3();
1259
+ println3(chalk2.cyan(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
1260
+ println3(
1261
+ chalk2.cyan(" \u2551") + chalk2.white.bold(" ToStudy MCP Auto-Repair ") + chalk2.cyan("\u2551")
1262
+ );
1263
+ println3(chalk2.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
1264
+ println3();
1265
+ if (report.repairsAttempted === 0) {
1266
+ println3(chalk2.green.bold("\u2713 Nenhum reparo necessario!"));
1267
+ println3();
1268
+ println3(chalk2.gray("Nenhum problema corrigivel automaticamente foi encontrado."));
1269
+ println3();
1270
+ return;
1271
+ }
1272
+ println3(chalk2.white.bold("Resumo:"));
1273
+ println3(` Reparos tentados: ${report.repairsAttempted}`);
1274
+ println3(` ${chalk2.green("\u2713 Sucesso:")} ${report.repairsSucceeded}`);
1275
+ if (report.repairsFailed > 0) {
1276
+ println3(` ${chalk2.red("\u2717 Falha:")} ${report.repairsFailed}`);
1277
+ }
1278
+ println3();
1279
+ println3(chalk2.white.bold("Detalhes:"));
1280
+ println3();
1281
+ for (const result of report.results) {
1282
+ if (result.success) {
1283
+ println3(` ${chalk2.green("\u2713")} ${result.message}`);
1284
+ if (result.action) {
1285
+ println3(chalk2.gray(` ${result.action}`));
1286
+ }
1287
+ } else {
1288
+ println3(` ${chalk2.red("\u2717")} ${result.message}`);
1289
+ }
1290
+ }
1291
+ println3();
1292
+ if (report.requiresUserInput.length > 0) {
1293
+ println3(chalk2.yellow("\u26A0 Alguns reparos requerem interacao:"));
1294
+ println3();
1295
+ println3(" Para reparar problemas de API key, execute:");
1296
+ println3(chalk2.cyan(" npx @tostudy-ai/mcp-setup repair --api-key <sua-api-key>"));
1297
+ println3();
1298
+ println3(" Para gerar uma nova API key:");
1299
+ println3(chalk2.cyan(" https://tostudy.ai/student/settings/mcp"));
1300
+ println3();
1301
+ }
1302
+ if (report.repairsFailed === 0 && report.requiresUserInput.length === 0) {
1303
+ println3(chalk2.green.bold("\u2713 Todos os reparos concluidos com sucesso!"));
1304
+ println3(chalk2.gray(" Reinicie o Claude Code para aplicar as mudancas."));
1305
+ } else if (report.repairsSucceeded > 0) {
1306
+ println3(chalk2.yellow("\u26A0 Alguns reparos foram concluidos, mas outros falharam."));
1307
+ println3(chalk2.gray(" Execute o diagnostico novamente para verificar o status."));
1308
+ } else {
1309
+ println3(chalk2.red("\u2717 Nenhum reparo foi concluido com sucesso."));
1310
+ println3(chalk2.gray(" Verifique os erros acima e tente novamente."));
1311
+ }
1312
+ println3();
1313
+ }
1314
+ if (process.argv[1]?.endsWith("repair.js")) {
1315
+ (async () => {
497
1316
  try {
498
- if (options.uninstall) {
499
- await uninstall();
500
- }
501
- else {
502
- await setup(options.apiKey, options.url);
1317
+ let apiKey;
1318
+ let platformUrl = DEFAULT_PLATFORM_URL;
1319
+ let jsonOutput = false;
1320
+ for (let i = 2; i < process.argv.length; i++) {
1321
+ const arg = process.argv[i];
1322
+ if (arg === "--api-key" && process.argv[i + 1]) {
1323
+ apiKey = process.argv[++i];
1324
+ } else if (arg === "--url" && process.argv[i + 1]) {
1325
+ platformUrl = process.argv[++i];
1326
+ } else if (arg === "--json") {
1327
+ jsonOutput = true;
1328
+ } else if (arg === "--help" || arg === "-h") {
1329
+ println3("ToStudy MCP Auto-Repair");
1330
+ println3();
1331
+ println3("Usage: node repair.js [options]");
1332
+ println3();
1333
+ println3("Options:");
1334
+ println3(" --api-key <key> API key para reparos que necessitam autenticacao");
1335
+ println3(" --url <url> URL da plataforma (default: https://tostudy.ai)");
1336
+ println3(" --json Saida em formato JSON");
1337
+ println3(" --help, -h Mostra esta ajuda");
1338
+ println3();
1339
+ process.exit(0);
503
1340
  }
1341
+ }
1342
+ if (!apiKey) {
1343
+ apiKey = process.env.TOSTUDY_API_KEY;
1344
+ }
1345
+ const { diagnostic, repair } = await diagnoseAndRepair(apiKey, platformUrl);
1346
+ if (jsonOutput) {
1347
+ println3(JSON.stringify({ diagnostic, repair }, null, 2));
1348
+ } else {
1349
+ printRepairReport(repair);
1350
+ }
1351
+ const hasUnfixedCritical = diagnostic.issues.some(
1352
+ (issue) => issue.severity === "critical" && !repair.results.find((r) => r.issueId === issue.id && r.success)
1353
+ );
1354
+ process.exit(hasUnfixedCritical ? 1 : 0);
1355
+ } catch (error) {
1356
+ process.stderr.write(chalk2.red("Erro ao executar reparo:\n"));
1357
+ process.stderr.write((error instanceof Error ? error.message : String(error)) + "\n");
1358
+ process.exit(1);
504
1359
  }
505
- catch (error) {
506
- eprintln();
507
- eprintln(chalk.red("Erro:") + " " + (error instanceof Error ? error.message : String(error)));
508
- process.exit(1);
1360
+ })();
1361
+ }
1362
+
1363
+ // src/ide-handlers/claude-code.ts
1364
+ import { execFileSync } from "child_process";
1365
+
1366
+ // src/ide-handlers/base.ts
1367
+ import { platform as platform3 } from "os";
1368
+ function buildMcpRemoteArgs(apiKey, mcpUrl, ide) {
1369
+ const osHeader = getClientOsHeaderValue2();
1370
+ return [
1371
+ "-y",
1372
+ "mcp-remote",
1373
+ `${mcpUrl}/mcp`,
1374
+ "--header",
1375
+ `Authorization:Bearer ${apiKey}`,
1376
+ "--header",
1377
+ `X-Tostudy-Client-IDE:${ide}`,
1378
+ "--header",
1379
+ `X-Tostudy-Client-OS:${osHeader}`
1380
+ ];
1381
+ }
1382
+ function buildTostudyServerEntry(apiKey, mcpUrl, ide) {
1383
+ return {
1384
+ command: "npx",
1385
+ args: buildMcpRemoteArgs(apiKey, mcpUrl, ide)
1386
+ };
1387
+ }
1388
+ function getClientOsHeaderValue2() {
1389
+ switch (platform3()) {
1390
+ case "darwin":
1391
+ return "macos";
1392
+ case "win32":
1393
+ return "windows";
1394
+ default:
1395
+ return "linux";
1396
+ }
1397
+ }
1398
+ async function verifyHeartbeat(apiKey, platformUrl) {
1399
+ try {
1400
+ const response = await fetch(`${platformUrl}/api/mcp/heartbeat`, {
1401
+ method: "POST",
1402
+ headers: {
1403
+ "Authorization": `Bearer ${apiKey}`,
1404
+ "Content-Type": "application/json"
1405
+ },
1406
+ body: JSON.stringify({ timestamp: (/* @__PURE__ */ new Date()).toISOString() })
1407
+ });
1408
+ return response.ok || response.status === 204;
1409
+ } catch {
1410
+ return false;
1411
+ }
1412
+ }
1413
+
1414
+ // src/ide-handlers/claude-code.ts
1415
+ function parseClaudeCodeScope(answer) {
1416
+ return answer.trim().toLowerCase().startsWith("l") ? "project" : "user";
1417
+ }
1418
+ async function promptClaudeCodeScope(promptFn = prompt) {
1419
+ const answer = await promptFn(
1420
+ " Claude Code: configurar global ou local? (g/l, padr\xE3o: global) "
1421
+ );
1422
+ return parseClaudeCodeScope(answer);
1423
+ }
1424
+ function buildClaudeCodeAddArgs(apiKey, mcpUrl, scope) {
1425
+ return [
1426
+ "mcp",
1427
+ "add",
1428
+ "tostudy",
1429
+ "--scope",
1430
+ scope,
1431
+ "--",
1432
+ "npx",
1433
+ ...buildMcpRemoteArgs(apiKey, mcpUrl, "claude-code")
1434
+ ];
1435
+ }
1436
+ function buildClaudeCodeRemoveArgs(scope) {
1437
+ return ["mcp", "remove", "tostudy", "--scope", scope];
1438
+ }
1439
+ var ClaudeCodeHandler = class {
1440
+ id = "claude-code";
1441
+ name = "Claude Code";
1442
+ async detect() {
1443
+ try {
1444
+ execFileSync("which", ["claude"], { stdio: "ignore" });
1445
+ return true;
1446
+ } catch {
1447
+ try {
1448
+ execFileSync("where", ["claude"], { stdio: "ignore" });
1449
+ return true;
1450
+ } catch {
1451
+ return false;
1452
+ }
509
1453
  }
510
- });
511
- // Wizard command - interactive step-by-step setup
512
- program
513
- .command("wizard")
514
- .description("Assistente interativo de configuracao passo a passo")
515
- .option("-k, --api-key <key>", "API key da plataforma ToStudy")
516
- .option("-u, --url <url>", "URL da plataforma (default: https://tostudy.ai)")
517
- .option("--skip-diagnostics", "Pula a verificacao de diagnostico apos setup")
518
- .option("--auto-repair", "Tenta reparar problemas automaticamente")
519
- .action(async (options) => {
1454
+ }
1455
+ getConfigPath() {
1456
+ return "Terminal (claude mcp add)";
1457
+ }
1458
+ async writeConfig(apiKey, mcpUrl) {
1459
+ const scope = await promptClaudeCodeScope();
1460
+ const addArgs = buildClaudeCodeAddArgs(apiKey, mcpUrl, scope);
1461
+ const removeArgs = buildClaudeCodeRemoveArgs(scope);
520
1462
  try {
521
- await wizard({
522
- apiKey: options.apiKey,
523
- url: options.url,
524
- skipDiagnostics: options.skipDiagnostics,
525
- autoRepair: options.autoRepair,
526
- });
527
- }
528
- catch (error) {
529
- eprintln();
530
- eprintln(chalk.red("Erro:") + " " + (error instanceof Error ? error.message : String(error)));
531
- process.exit(1);
1463
+ try {
1464
+ execFileSync("claude", removeArgs, { stdio: "ignore" });
1465
+ } catch {
1466
+ }
1467
+ execFileSync("claude", addArgs, { stdio: "inherit" });
1468
+ } catch (error) {
1469
+ throw new Error(
1470
+ `Failed to run 'claude mcp add'. Is Claude Code installed?
1471
+ Error: ${error instanceof Error ? error.message : String(error)}`
1472
+ );
532
1473
  }
533
- });
534
- // Diagnose command
535
- program
536
- .command("diagnose")
537
- .description("Executa diagnostico de problemas na configuracao MCP")
538
- .option("--json", "Saida em formato JSON")
539
- .action(async (options) => {
1474
+ }
1475
+ async verify(apiKey, mcpUrl) {
1476
+ return verifyHeartbeat(apiKey, mcpUrl);
1477
+ }
1478
+ };
1479
+
1480
+ // src/ide-handlers/cursor.ts
1481
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
1482
+ import { join as join3, dirname as dirname3 } from "path";
1483
+ import { homedir as homedir3, platform as platform4 } from "os";
1484
+ var CursorHandler = class {
1485
+ id = "cursor";
1486
+ name = "Cursor";
1487
+ async detect() {
1488
+ const home = homedir3();
1489
+ const os = platform4();
1490
+ switch (os) {
1491
+ case "darwin":
1492
+ return existsSync5(join3("/Applications", "Cursor.app")) || existsSync5(join3(home, "Applications", "Cursor.app"));
1493
+ case "win32": {
1494
+ const localAppData = process.env.LOCALAPPDATA || join3(home, "AppData", "Local");
1495
+ return existsSync5(join3(localAppData, "Programs", "cursor", "Cursor.exe"));
1496
+ }
1497
+ case "linux":
1498
+ return existsSync5(join3(home, ".cursor"));
1499
+ default:
1500
+ return false;
1501
+ }
1502
+ }
1503
+ getConfigPath() {
1504
+ const home = homedir3();
1505
+ return join3(home, ".cursor", "mcp.json");
1506
+ }
1507
+ async writeConfig(apiKey, mcpUrl) {
1508
+ const configPath = this.getConfigPath();
1509
+ const configDir = dirname3(configPath);
1510
+ if (!existsSync5(configDir)) {
1511
+ mkdirSync3(configDir, { recursive: true });
1512
+ }
1513
+ let config = {};
1514
+ if (existsSync5(configPath)) {
1515
+ try {
1516
+ config = JSON.parse(readFileSync3(configPath, "utf-8"));
1517
+ } catch {
1518
+ }
1519
+ }
1520
+ const mcpServers = config.mcpServers ?? {};
1521
+ mcpServers["tostudy"] = buildTostudyServerEntry(apiKey, mcpUrl, this.id);
1522
+ config.mcpServers = mcpServers;
1523
+ writeFileSync3(configPath, JSON.stringify(config, null, 2), "utf-8");
1524
+ }
1525
+ async verify(apiKey, mcpUrl) {
1526
+ return verifyHeartbeat(apiKey, mcpUrl);
1527
+ }
1528
+ };
1529
+
1530
+ // src/ide-handlers/vscode.ts
1531
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
1532
+ import { join as join4, dirname as dirname4 } from "path";
1533
+ import { homedir as homedir4, platform as platform5 } from "os";
1534
+ var VSCodeHandler = class {
1535
+ id = "vscode";
1536
+ name = "VS Code";
1537
+ async detect() {
1538
+ const home = homedir4();
1539
+ const os = platform5();
1540
+ switch (os) {
1541
+ case "darwin":
1542
+ return existsSync6(join4("/Applications", "Visual Studio Code.app")) || existsSync6(join4(home, "Applications", "Visual Studio Code.app"));
1543
+ case "win32": {
1544
+ const programFiles = process.env.ProgramFiles || "C:\\Program Files";
1545
+ const localAppData = process.env.LOCALAPPDATA || join4(home, "AppData", "Local");
1546
+ return existsSync6(join4(programFiles, "Microsoft VS Code", "Code.exe")) || existsSync6(join4(localAppData, "Programs", "Microsoft VS Code", "Code.exe"));
1547
+ }
1548
+ case "linux":
1549
+ return existsSync6("/usr/bin/code") || existsSync6("/usr/local/bin/code");
1550
+ default:
1551
+ return false;
1552
+ }
1553
+ }
1554
+ getConfigPath() {
1555
+ return join4(process.cwd(), ".vscode", "mcp.json");
1556
+ }
1557
+ async writeConfig(apiKey, mcpUrl) {
1558
+ const configPath = this.getConfigPath();
1559
+ const configDir = dirname4(configPath);
1560
+ if (!existsSync6(configDir)) {
1561
+ mkdirSync4(configDir, { recursive: true });
1562
+ }
1563
+ let config = {};
1564
+ if (existsSync6(configPath)) {
1565
+ try {
1566
+ config = JSON.parse(readFileSync4(configPath, "utf-8"));
1567
+ } catch {
1568
+ }
1569
+ }
1570
+ const servers = config.servers ?? {};
1571
+ servers["tostudy"] = buildTostudyServerEntry(apiKey, mcpUrl, this.id);
1572
+ config.servers = servers;
1573
+ writeFileSync4(configPath, JSON.stringify(config, null, 2), "utf-8");
1574
+ }
1575
+ async verify(apiKey, mcpUrl) {
1576
+ return verifyHeartbeat(apiKey, mcpUrl);
1577
+ }
1578
+ };
1579
+
1580
+ // src/ide-handlers/desktop.ts
1581
+ import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, copyFileSync as copyFileSync3 } from "fs";
1582
+ import { dirname as dirname5 } from "path";
1583
+ var DesktopHandler = class {
1584
+ id = "desktop";
1585
+ name = "Claude Desktop";
1586
+ async detect() {
1587
+ return isClaudeInstalled();
1588
+ }
1589
+ getConfigPath() {
1590
+ return getClaudeConfigPath();
1591
+ }
1592
+ async writeConfig(apiKey, mcpUrl) {
1593
+ const configPath = this.getConfigPath();
1594
+ const configDir = dirname5(configPath);
1595
+ if (!existsSync7(configDir)) {
1596
+ mkdirSync5(configDir, { recursive: true });
1597
+ }
1598
+ let config = {};
1599
+ if (existsSync7(configPath)) {
1600
+ try {
1601
+ config = JSON.parse(readFileSync5(configPath, "utf-8"));
1602
+ } catch {
1603
+ }
1604
+ const backupPath = `${configPath}.backup`;
1605
+ copyFileSync3(configPath, backupPath);
1606
+ }
1607
+ const mcpServers = config.mcpServers ?? {};
1608
+ mcpServers["tostudy"] = buildTostudyServerEntry(apiKey, mcpUrl, this.id);
1609
+ config.mcpServers = mcpServers;
1610
+ writeFileSync5(configPath, JSON.stringify(config, null, 2), "utf-8");
1611
+ }
1612
+ async verify(apiKey, mcpUrl) {
1613
+ return verifyHeartbeat(apiKey, mcpUrl);
1614
+ }
1615
+ };
1616
+
1617
+ // src/ide-handlers/windsurf.ts
1618
+ import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
1619
+ import { join as join5, dirname as dirname6 } from "path";
1620
+ import { homedir as homedir5 } from "os";
1621
+ var WindsurfHandler = class {
1622
+ id = "windsurf";
1623
+ name = "Windsurf";
1624
+ async detect() {
1625
+ const home = homedir5();
1626
+ return existsSync8(join5(home, ".codeium", "windsurf")) || existsSync8(join5(home, ".codeium", "windsurf", "mcp_config.json"));
1627
+ }
1628
+ getConfigPath() {
1629
+ return join5(homedir5(), ".codeium", "windsurf", "mcp_config.json");
1630
+ }
1631
+ async writeConfig(apiKey, mcpUrl) {
1632
+ const configPath = this.getConfigPath();
1633
+ const configDir = dirname6(configPath);
1634
+ if (!existsSync8(configDir)) {
1635
+ mkdirSync6(configDir, { recursive: true });
1636
+ }
1637
+ let config = {};
1638
+ if (existsSync8(configPath)) {
1639
+ try {
1640
+ config = JSON.parse(readFileSync6(configPath, "utf-8"));
1641
+ } catch {
1642
+ }
1643
+ }
1644
+ const mcpServers = config.mcpServers ?? {};
1645
+ mcpServers.tostudy = buildTostudyServerEntry(apiKey, mcpUrl, this.id);
1646
+ config.mcpServers = mcpServers;
1647
+ writeFileSync6(configPath, JSON.stringify(config, null, 2), "utf-8");
1648
+ }
1649
+ async verify(apiKey, mcpUrl) {
1650
+ return verifyHeartbeat(apiKey, mcpUrl);
1651
+ }
1652
+ };
1653
+
1654
+ // src/ide-handlers/opencode.ts
1655
+ import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
1656
+ import { execFileSync as execFileSync2 } from "child_process";
1657
+ import { join as join6, dirname as dirname7 } from "path";
1658
+ import { homedir as homedir6 } from "os";
1659
+ var OpenCodeHandler = class {
1660
+ id = "opencode";
1661
+ name = "OpenCode";
1662
+ async detect() {
540
1663
  try {
541
- const report = await runDiagnostics();
542
- if (options.json) {
543
- println(JSON.stringify(report, null, 2));
544
- }
545
- else {
546
- printDiagnosticReport(report);
547
- }
548
- process.exit(report.passed ? 0 : 1);
1664
+ execFileSync2("which", ["opencode"], { stdio: "ignore" });
1665
+ return true;
1666
+ } catch {
1667
+ try {
1668
+ execFileSync2("where", ["opencode"], { stdio: "ignore" });
1669
+ return true;
1670
+ } catch {
1671
+ return existsSync9(join6(homedir6(), ".opencode"));
1672
+ }
549
1673
  }
550
- catch (error) {
551
- eprintln();
552
- eprintln(chalk.red("Erro ao executar diagnostico:"));
553
- eprintln(error instanceof Error ? error.message : String(error));
554
- process.exit(1);
1674
+ }
1675
+ getConfigPath() {
1676
+ return join6(homedir6(), ".opencode", "opencode.json");
1677
+ }
1678
+ async writeConfig(apiKey, mcpUrl) {
1679
+ const configPath = this.getConfigPath();
1680
+ const configDir = dirname7(configPath);
1681
+ if (!existsSync9(configDir)) {
1682
+ mkdirSync7(configDir, { recursive: true });
555
1683
  }
556
- });
557
- // Repair command
558
- program
559
- .command("repair")
560
- .description("Repara problemas de configuracao automaticamente")
561
- .option("-k, --api-key <key>", "API key para reparos que necessitam autenticacao")
562
- .option("-u, --url <url>", "URL da plataforma (default: https://tostudy.ai)")
563
- .option("--json", "Saida em formato JSON")
564
- .action(async (options) => {
1684
+ let config = {};
1685
+ if (existsSync9(configPath)) {
1686
+ try {
1687
+ config = JSON.parse(readFileSync7(configPath, "utf-8"));
1688
+ } catch {
1689
+ }
1690
+ }
1691
+ const mcp = config.mcp ?? {};
1692
+ mcp["tostudy"] = buildTostudyServerEntry(apiKey, mcpUrl, this.id);
1693
+ config.mcp = mcp;
1694
+ writeFileSync7(configPath, JSON.stringify(config, null, 2), "utf-8");
1695
+ }
1696
+ async verify(apiKey, mcpUrl) {
1697
+ return verifyHeartbeat(apiKey, mcpUrl);
1698
+ }
1699
+ };
1700
+
1701
+ // src/ide-handlers/codex.ts
1702
+ import { execFileSync as execFileSync3 } from "child_process";
1703
+ import { existsSync as existsSync10 } from "fs";
1704
+ import { join as join7 } from "path";
1705
+ import { homedir as homedir7 } from "os";
1706
+ var CodexHandler = class {
1707
+ id = "codex";
1708
+ name = "Codex";
1709
+ async detect() {
565
1710
  try {
566
- const apiKey = options.apiKey || process.env.TOSTUDY_API_KEY;
567
- const platformUrl = options.url || DEFAULT_PLATFORM_URL;
568
- const diagnostic = await runDiagnostics();
569
- const repair = repairAllIssues(diagnostic, apiKey, platformUrl);
570
- if (options.json) {
571
- println(JSON.stringify({ diagnostic, repair }, null, 2));
572
- }
573
- else {
574
- printRepairReport(repair);
1711
+ execFileSync3("which", ["codex"], { stdio: "ignore" });
1712
+ return true;
1713
+ } catch {
1714
+ try {
1715
+ execFileSync3("where", ["codex"], { stdio: "ignore" });
1716
+ return true;
1717
+ } catch {
1718
+ return existsSync10(join7(homedir7(), ".codex"));
1719
+ }
1720
+ }
1721
+ }
1722
+ getConfigPath() {
1723
+ return join7(homedir7(), ".codex", "config.toml");
1724
+ }
1725
+ async writeConfig(apiKey, mcpUrl) {
1726
+ const args = buildMcpRemoteArgs(apiKey, mcpUrl, this.id);
1727
+ try {
1728
+ try {
1729
+ execFileSync3("codex", ["mcp", "remove", "tostudy"], { stdio: "ignore" });
1730
+ } catch {
1731
+ }
1732
+ execFileSync3("codex", ["mcp", "add", "tostudy", "--", "npx", ...args], { stdio: "inherit" });
1733
+ } catch (error) {
1734
+ throw new Error(
1735
+ `Failed to run 'codex mcp add'. Is Codex installed?
1736
+ Error: ${error instanceof Error ? error.message : String(error)}`
1737
+ );
1738
+ }
1739
+ }
1740
+ async verify(apiKey, mcpUrl) {
1741
+ return verifyHeartbeat(apiKey, mcpUrl);
1742
+ }
1743
+ };
1744
+
1745
+ // src/ide-handlers/antigravity.ts
1746
+ import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8 } from "fs";
1747
+ import { join as join8, dirname as dirname8 } from "path";
1748
+ import { homedir as homedir8 } from "os";
1749
+ var AntigravityHandler = class {
1750
+ id = "antigravity";
1751
+ name = "Antigravity";
1752
+ async detect() {
1753
+ const home = homedir8();
1754
+ return existsSync11(join8(home, ".gemini", "antigravity")) || existsSync11(join8(home, ".gemini", "antigravity", "mcp_config.json"));
1755
+ }
1756
+ getConfigPath() {
1757
+ return join8(homedir8(), ".gemini", "antigravity", "mcp_config.json");
1758
+ }
1759
+ async writeConfig(apiKey, mcpUrl) {
1760
+ const configPath = this.getConfigPath();
1761
+ const configDir = dirname8(configPath);
1762
+ if (!existsSync11(configDir)) {
1763
+ mkdirSync8(configDir, { recursive: true });
1764
+ }
1765
+ let config = {};
1766
+ if (existsSync11(configPath)) {
1767
+ try {
1768
+ config = JSON.parse(readFileSync8(configPath, "utf-8"));
1769
+ } catch {
1770
+ }
1771
+ }
1772
+ const mcpServers = config.mcpServers ?? {};
1773
+ mcpServers.tostudy = buildTostudyServerEntry(apiKey, mcpUrl, this.id);
1774
+ config.mcpServers = mcpServers;
1775
+ writeFileSync8(configPath, JSON.stringify(config, null, 2), "utf-8");
1776
+ }
1777
+ async verify(apiKey, mcpUrl) {
1778
+ return verifyHeartbeat(apiKey, mcpUrl);
1779
+ }
1780
+ };
1781
+
1782
+ // src/ide-handlers/manual.ts
1783
+ import chalk3 from "chalk";
1784
+ var TOKEN_PLACEHOLDER = "<SUBSTITUA_PELO_SEU_TOKEN_TOSTUDY>";
1785
+ var ManualHandler = class {
1786
+ id = "manual";
1787
+ name = "Manual";
1788
+ async detect() {
1789
+ return true;
1790
+ }
1791
+ getConfigPath() {
1792
+ return "stdout";
1793
+ }
1794
+ async writeConfig(_apiKey, mcpUrl) {
1795
+ const config = {
1796
+ mcpServers: {
1797
+ tostudy: buildTostudyServerEntry(TOKEN_PLACEHOLDER, mcpUrl, this.id)
1798
+ }
1799
+ };
1800
+ process.stdout.write("\n");
1801
+ process.stdout.write(
1802
+ chalk3.cyan(
1803
+ "Add the following to your MCP config and replace the token placeholder manually:\n"
1804
+ )
1805
+ );
1806
+ process.stdout.write("\n");
1807
+ process.stdout.write(JSON.stringify(config, null, 2));
1808
+ process.stdout.write("\n\n");
1809
+ }
1810
+ async verify(apiKey, mcpUrl) {
1811
+ return verifyHeartbeat(apiKey, mcpUrl);
1812
+ }
1813
+ };
1814
+
1815
+ // src/ide-handlers/index.ts
1816
+ var handlers = {
1817
+ "claude-code": () => new ClaudeCodeHandler(),
1818
+ "cursor": () => new CursorHandler(),
1819
+ "vscode": () => new VSCodeHandler(),
1820
+ "desktop": () => new DesktopHandler(),
1821
+ "windsurf": () => new WindsurfHandler(),
1822
+ "opencode": () => new OpenCodeHandler(),
1823
+ "codex": () => new CodexHandler(),
1824
+ "antigravity": () => new AntigravityHandler(),
1825
+ "manual": () => new ManualHandler()
1826
+ };
1827
+ function getIDEHandler(ide) {
1828
+ const factory = handlers[ide];
1829
+ if (!factory) {
1830
+ throw new Error(`Unknown IDE: ${ide}. Supported: ${Object.keys(handlers).join(", ")}`);
1831
+ }
1832
+ return factory();
1833
+ }
1834
+ function getAllIDEHandlers() {
1835
+ return Object.values(handlers).map((factory) => factory());
1836
+ }
1837
+ async function detectInstalledIDEs() {
1838
+ const all = getAllIDEHandlers();
1839
+ const results = [];
1840
+ for (const handler of all) {
1841
+ if (await handler.detect()) {
1842
+ results.push(handler);
1843
+ }
1844
+ }
1845
+ return results;
1846
+ }
1847
+
1848
+ // src/messages.ts
1849
+ var NEXT_STEPS_TITLE = " Pr\xF3ximos passos:";
1850
+ var ASSISTANT_COURSES_PROMPT = ' Pe\xE7a ao assistente: "mostre meus cursos"';
1851
+ var COURSES_TOOL_HINT = ' Use a ferramenta "courses" para ver seus cursos';
1852
+ var API_KEY_DEPRECATION_WARNING = "\u26A0 The --api-key flag is deprecated and will be removed in a future version. Use the default OAuth flow instead: npx @tostudy-ai/mcp-setup";
1853
+
1854
+ // src/oauth-server.ts
1855
+ import http from "http";
1856
+
1857
+ // src/callback-page.ts
1858
+ function getCallbackPageHtml() {
1859
+ return `<!DOCTYPE html>
1860
+ <html lang="pt-BR">
1861
+ <head>
1862
+ <meta charset="utf-8">
1863
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1864
+ <title>ToStudy \u2014 Autenticado</title>
1865
+ <style>
1866
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1867
+ body {
1868
+ background: #0a0a0a;
1869
+ color: #fafafa;
1870
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1871
+ display: flex;
1872
+ align-items: center;
1873
+ justify-content: center;
1874
+ min-height: 100vh;
1875
+ }
1876
+ .container {
1877
+ text-align: center;
1878
+ padding: 2rem;
1879
+ }
1880
+ .checkmark-circle {
1881
+ width: 80px;
1882
+ height: 80px;
1883
+ border-radius: 50%;
1884
+ border: 3px solid #22d3ee;
1885
+ display: flex;
1886
+ align-items: center;
1887
+ justify-content: center;
1888
+ margin: 0 auto 1.5rem;
1889
+ animation: scaleIn 0.4s ease-out;
1890
+ }
1891
+ .checkmark {
1892
+ width: 40px;
1893
+ height: 40px;
1894
+ stroke: #22d3ee;
1895
+ stroke-width: 3;
1896
+ fill: none;
1897
+ stroke-linecap: round;
1898
+ stroke-linejoin: round;
1899
+ stroke-dasharray: 50;
1900
+ stroke-dashoffset: 50;
1901
+ animation: draw 0.5s ease-out 0.3s forwards;
1902
+ }
1903
+ h1 {
1904
+ font-size: 1.5rem;
1905
+ font-weight: 600;
1906
+ margin-bottom: 0.5rem;
1907
+ }
1908
+ .subtitle {
1909
+ color: #a1a1aa;
1910
+ font-size: 0.95rem;
1911
+ margin-bottom: 0.25rem;
1912
+ }
1913
+ .brand {
1914
+ color: #22d3ee;
1915
+ font-size: 0.85rem;
1916
+ margin-top: 2rem;
1917
+ letter-spacing: 0.05em;
1918
+ }
1919
+ @keyframes scaleIn {
1920
+ from { transform: scale(0); opacity: 0; }
1921
+ to { transform: scale(1); opacity: 1; }
1922
+ }
1923
+ @keyframes draw {
1924
+ to { stroke-dashoffset: 0; }
1925
+ }
1926
+ </style>
1927
+ </head>
1928
+ <body>
1929
+ <div class="container">
1930
+ <div class="checkmark-circle">
1931
+ <svg class="checkmark" viewBox="0 0 40 40">
1932
+ <polyline points="12,20 18,26 28,14" />
1933
+ </svg>
1934
+ </div>
1935
+ <h1>Autenticado com sucesso!</h1>
1936
+ <p class="subtitle">Pode voltar ao terminal.</p>
1937
+ <p class="subtitle" id="close-msg">Esta aba ser\xE1 fechada automaticamente.</p>
1938
+ <p class="brand">ToStudy</p>
1939
+ </div>
1940
+ <script>
1941
+ setTimeout(function() {
1942
+ window.close();
1943
+ document.getElementById('close-msg').textContent = 'Pode fechar esta aba manualmente.';
1944
+ }, 3000);
1945
+ </script>
1946
+ </body>
1947
+ </html>`;
1948
+ }
1949
+
1950
+ // src/oauth-server.ts
1951
+ function startCallbackServer(port) {
1952
+ return new Promise((resolve, reject) => {
1953
+ const server = http.createServer((req, res) => {
1954
+ const url = new URL(req.url, `http://localhost:${port}`);
1955
+ if (url.pathname === "/callback") {
1956
+ const code = url.searchParams.get("code");
1957
+ if (code) {
1958
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1959
+ res.end(getCallbackPageHtml());
1960
+ server.close();
1961
+ resolve({ code });
1962
+ } else {
1963
+ res.writeHead(400, { "Content-Type": "text/plain" });
1964
+ res.end("Missing code");
575
1965
  }
576
- const hasUnfixedCritical = diagnostic.issues.some((issue) => issue.severity === "critical" &&
577
- !repair.results.find((r) => r.issueId === issue.id && r.success));
578
- process.exit(hasUnfixedCritical ? 1 : 0);
1966
+ } else {
1967
+ res.writeHead(404, { "Content-Type": "text/plain" });
1968
+ res.end("Not found");
1969
+ }
1970
+ });
1971
+ server.listen(port, () => {
1972
+ });
1973
+ server.on("error", (err) => {
1974
+ if (err.code === "EADDRINUSE") {
1975
+ reject(new Error(`Porta ${port} em uso. Tente novamente ou use --api-key.`));
1976
+ } else {
1977
+ reject(err);
1978
+ }
1979
+ });
1980
+ const timeoutId = setTimeout(() => {
1981
+ server.close();
1982
+ reject(new Error("Login timeout \u2014 nenhuma resposta em 2 minutos."));
1983
+ }, 12e4);
1984
+ timeoutId.unref();
1985
+ });
1986
+ }
1987
+
1988
+ // src/commands/init.ts
1989
+ import { existsSync as existsSync13, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
1990
+ import { dirname as dirname9, join as join10 } from "path";
1991
+ import chalk4 from "chalk";
1992
+
1993
+ // ../mcp-cli-core/src/detect-ide.ts
1994
+ var IDE_DETECTION_RULES = [
1995
+ {
1996
+ id: "claude-code",
1997
+ name: "Claude Code",
1998
+ envVars: ["CLAUDE_CODE"]
1999
+ },
2000
+ {
2001
+ id: "cursor",
2002
+ name: "Cursor",
2003
+ envVars: ["CURSOR_TRACE_ID", "CURSOR_CHANNEL"]
2004
+ },
2005
+ {
2006
+ id: "codex",
2007
+ name: "Codex",
2008
+ envVars: ["CODEX"]
2009
+ },
2010
+ {
2011
+ id: "windsurf",
2012
+ name: "Windsurf",
2013
+ envVars: ["WINDSURF_PID"]
2014
+ },
2015
+ {
2016
+ id: "vscode",
2017
+ name: "VS Code",
2018
+ envVars: ["VSCODE_PID", "VSCODE_IPC_HOOK"]
2019
+ }
2020
+ ];
2021
+ async function detectRunningIDE() {
2022
+ for (const rule of IDE_DETECTION_RULES) {
2023
+ if (rule.envVars.some((envVar) => process.env[envVar] !== void 0)) {
2024
+ return {
2025
+ id: rule.id,
2026
+ name: rule.name,
2027
+ detected: true
2028
+ };
579
2029
  }
580
- catch (error) {
581
- eprintln();
582
- eprintln(chalk.red("Erro ao executar reparo:"));
583
- eprintln(error instanceof Error ? error.message : String(error));
584
- process.exit(1);
2030
+ }
2031
+ if (process.env.TERM_PROGRAM?.toLowerCase().includes("vscode")) {
2032
+ return {
2033
+ id: "vscode",
2034
+ name: "VS Code",
2035
+ detected: true
2036
+ };
2037
+ }
2038
+ return null;
2039
+ }
2040
+
2041
+ // ../mcp-cli-core/src/detect-node.ts
2042
+ import * as childProcess from "child_process";
2043
+
2044
+ // ../mcp-cli-core/src/detect-workspace.ts
2045
+ import { existsSync as existsSync12 } from "fs";
2046
+ import { join as join9 } from "path";
2047
+ async function detectWorkspaceScenario(cwd) {
2048
+ const hasAgentsMd = existsSync12(join9(cwd, "AGENTS.md")) || existsSync12(join9(cwd, "CLAUDE.md"));
2049
+ const hasCursorRules = existsSync12(join9(cwd, ".cursor", "rules"));
2050
+ const hasCopilotInstructions = existsSync12(join9(cwd, ".github", "copilot-instructions.md"));
2051
+ return {
2052
+ scenario: hasAgentsMd ? "existing" : "empty",
2053
+ cwd,
2054
+ hasAgentsMd,
2055
+ hasCursorRules,
2056
+ hasCopilotInstructions
2057
+ };
2058
+ }
2059
+
2060
+ // ../mcp-cli-core/src/generate-instructions.ts
2061
+ var CLI_COMMANDS = [
2062
+ { command: "tostudy start", description: "Carrega o pr\xF3ximo m\xF3dulo", category: "learning" },
2063
+ { command: "tostudy next", description: "Avan\xE7a para a pr\xF3xima li\xE7\xE3o", category: "learning" },
2064
+ {
2065
+ command: "tostudy hint",
2066
+ description: "Mostra dicas progressivas para o exerc\xEDcio atual",
2067
+ category: "learning"
2068
+ },
2069
+ {
2070
+ command: "tostudy validate",
2071
+ description: "Valida o exerc\xEDcio antes de avan\xE7ar",
2072
+ category: "learning"
2073
+ },
2074
+ { command: "tostudy progress", description: "Mostra o progresso do curso", category: "learning" },
2075
+ {
2076
+ command: "tostudy workspace",
2077
+ description: "Cria ou atualiza o workspace local",
2078
+ category: "workspace"
2079
+ }
2080
+ ];
2081
+ function formatModuleMarker(module, currentModule) {
2082
+ if (module.completed) {
2083
+ return "\u2705";
2084
+ }
2085
+ if (module.order === currentModule) {
2086
+ return "\u{1F504}";
2087
+ }
2088
+ return "\u2B1A";
2089
+ }
2090
+ function formatModuleList(modules, currentModule) {
2091
+ return [...modules].sort((left, right) => left.order - right.order).map((module) => {
2092
+ const suffix = module.order === currentModule ? " (atual)" : "";
2093
+ return `${module.order}. ${formatModuleMarker(module, currentModule)} ${module.title}${suffix}`;
2094
+ }).join("\n");
2095
+ }
2096
+ function formatToolList(tools) {
2097
+ return tools.map((tool) => `- \`${tool.command}\` \u2014 ${tool.description}`).join("\n");
2098
+ }
2099
+ function generateMcpContent(course) {
2100
+ const learningTools = course.availableTools.filter((tool) => tool.category === "learning");
2101
+ const workspaceTools = course.availableTools.filter((tool) => tool.category === "workspace");
2102
+ const hintTool = learningTools.find((tool) => tool.command.includes("hint"));
2103
+ const validateTool = learningTools.find((tool) => tool.command.includes("validate"));
2104
+ return `# ${course.title} \u2014 Guia de Estudo
2105
+
2106
+ ## Progresso Atual
2107
+
2108
+ - M\xF3dulo ${course.currentModule} de ${course.totalModules} (${course.progressPercent}% completo)
2109
+ - Li\xE7\xE3o atual: ${course.currentModule}.${course.currentLesson} \u2014 ${course.currentLessonTitle}
2110
+
2111
+ ## Tools Dispon\xEDveis (MCP)
2112
+
2113
+ ### Aprendizado
2114
+
2115
+ ${formatToolList(learningTools)}
2116
+
2117
+ ${workspaceTools.length > 0 ? `### Workspace
2118
+
2119
+ ${formatToolList(workspaceTools)}
2120
+
2121
+ ` : ""}## Como Guiar o Aluno
2122
+
2123
+ - Apresente o conte\xFAdo da li\xE7\xE3o antes de propor exerc\xEDcios
2124
+ ${hintTool ? `- Use \`${hintTool.command}\` antes de entregar a resposta` : ""}
2125
+ ${validateTool ? `- Valide com \`${validateTool.command}\` antes de avan\xE7ar` : ""}
2126
+ - Respeite a sequ\xEAncia dos m\xF3dulos
2127
+
2128
+ ## M\xF3dulos
2129
+
2130
+ ${formatModuleList(course.modules, course.currentModule)}
2131
+
2132
+ ---
2133
+
2134
+ _ToStudy Platform \u2014 Criado por ${course.creator.name}_
2135
+ _Atualizado via \`tostudy sync\`_`;
2136
+ }
2137
+ function generateCliContent(course) {
2138
+ const learningCommands = CLI_COMMANDS.filter((tool) => tool.category === "learning");
2139
+ const workspaceCommands = CLI_COMMANDS.filter((tool) => tool.category === "workspace");
2140
+ return `# ${course.title} \u2014 Guia de Estudo
2141
+
2142
+ ## Progresso Atual
2143
+
2144
+ - M\xF3dulo ${course.currentModule} de ${course.totalModules} (${course.progressPercent}% completo)
2145
+ - Li\xE7\xE3o atual: ${course.currentModule}.${course.currentLesson} \u2014 ${course.currentLessonTitle}
2146
+
2147
+ ## Comandos CLI Dispon\xEDveis
2148
+
2149
+ ### Aprendizado
2150
+
2151
+ ${formatToolList(learningCommands)}
2152
+
2153
+ ### Workspace
2154
+
2155
+ ${formatToolList(workspaceCommands)}
2156
+
2157
+ ## Como Guiar o Aluno
2158
+
2159
+ - Instrua o aluno a rodar comandos no terminal
2160
+ - Leia a sa\xEDda dos comandos para orientar o pr\xF3ximo passo
2161
+ - Use \`tostudy hint\` antes de dar respostas diretas
2162
+ - Respeite a sequ\xEAncia dos m\xF3dulos
2163
+
2164
+ ## M\xF3dulos
2165
+
2166
+ ${formatModuleList(course.modules, course.currentModule)}
2167
+
2168
+ ---
2169
+
2170
+ _ToStudy Platform \u2014 Criado por ${course.creator.name}_
2171
+ _Atualizado via \`tostudy sync\`_`;
2172
+ }
2173
+ function generateInstructions(course, options) {
2174
+ if (options.mode === "cli") {
2175
+ return generateCliContent(course);
2176
+ }
2177
+ return generateMcpContent(course);
2178
+ }
2179
+
2180
+ // ../mcp-cli-core/src/render-for-ide.ts
2181
+ function getAgentsMdPath(scenario) {
2182
+ return scenario === "existing" ? ".tostudy/AGENTS.md" : "AGENTS.md";
2183
+ }
2184
+ function renderCursorMdc(content) {
2185
+ const title = content.match(/^# (.+)$/m)?.[1] ?? "ToStudy Course Guide";
2186
+ return {
2187
+ filePath: ".cursor/rules/tostudy.mdc",
2188
+ content: `---
2189
+ description: ${title}
2190
+ globs: ["**/*"]
2191
+ alwaysApply: true
2192
+ ---
2193
+
2194
+ ${content}`
2195
+ };
2196
+ }
2197
+ function renderForIDE(content, ide, scenario) {
2198
+ switch (ide) {
2199
+ case "cursor":
2200
+ return renderCursorMdc(content);
2201
+ case "copilot":
2202
+ return {
2203
+ filePath: ".github/copilot-instructions.md",
2204
+ content
2205
+ };
2206
+ case "claude-code":
2207
+ case "codex":
2208
+ case "vscode":
2209
+ case "windsurf":
2210
+ case "opencode":
2211
+ case "antigravity":
2212
+ case "manual":
2213
+ default:
2214
+ return {
2215
+ filePath: getAgentsMdPath(scenario),
2216
+ content
2217
+ };
2218
+ }
2219
+ }
2220
+
2221
+ // src/commands/shared.ts
2222
+ var COURSE_ID_COMMENT_PREFIX = "<!-- tostudy-course-id:";
2223
+ var DEFAULT_AVAILABLE_TOOLS = [
2224
+ { command: "start_module", description: "Carrega o pr\xF3ximo m\xF3dulo", category: "learning" },
2225
+ { command: "next_lesson", description: "Avan\xE7a para a pr\xF3xima li\xE7\xE3o", category: "learning" },
2226
+ { command: "get_hint", description: "Mostra dicas progressivas", category: "learning" },
2227
+ { command: "validate_solution", description: "Valida o exerc\xEDcio atual", category: "learning" },
2228
+ { command: "get_progress", description: "Mostra o progresso do curso", category: "learning" },
2229
+ {
2230
+ command: "setup_workspace",
2231
+ description: "Configura o workspace do aluno",
2232
+ category: "workspace"
2233
+ }
2234
+ ];
2235
+ function normalizeCourseContext(course) {
2236
+ if ("availableTools" in course) {
2237
+ return course;
2238
+ }
2239
+ return {
2240
+ id: course.id,
2241
+ title: course.title,
2242
+ description: course.description ?? "",
2243
+ modules: course.modules,
2244
+ currentModule: course.currentModule,
2245
+ currentLesson: course.currentLesson,
2246
+ currentLessonTitle: course.currentLessonTitle,
2247
+ progressPercent: course.progressPercent,
2248
+ totalModules: course.totalModules || course.modules.length || 1,
2249
+ creator: { name: course.creator.name },
2250
+ availableTools: DEFAULT_AVAILABLE_TOOLS
2251
+ };
2252
+ }
2253
+ async function fetchCourses(apiKey, platformUrl) {
2254
+ try {
2255
+ const baseUrl = platformUrl.replace(/\/$/, "");
2256
+ const response = await fetch(`${baseUrl}/api/mcp/courses`, {
2257
+ headers: {
2258
+ Authorization: `Bearer ${apiKey}`,
2259
+ "Content-Type": "application/json"
2260
+ }
2261
+ });
2262
+ if (!response.ok) {
2263
+ return [];
585
2264
  }
586
- });
587
- // Install command - non-interactive, used by the web wizard's npx command
588
- const SUPPORTED_IDES = [
589
- "claude-code",
590
- "cursor",
591
- "vscode",
592
- "desktop",
593
- "windsurf",
594
- "opencode",
595
- "codex",
596
- "antigravity",
597
- "manual",
2265
+ const data = await response.json();
2266
+ return (Array.isArray(data.courses) ? data.courses : []).map(
2267
+ (course) => normalizeCourseContext(course)
2268
+ );
2269
+ } catch {
2270
+ return [];
2271
+ }
2272
+ }
2273
+ async function fetchCourseContext(courseId, apiKey, platformUrl) {
2274
+ const courses = await fetchCourses(apiKey, platformUrl);
2275
+ return courses.find((course) => course.id === courseId) ?? null;
2276
+ }
2277
+ async function detectInstructionMode(apiKey, platformUrl, verifyHeartbeat2) {
2278
+ return await verifyHeartbeat2(apiKey, platformUrl) ? "mcp" : "cli";
2279
+ }
2280
+ function appendCourseMetadata(content, courseId) {
2281
+ return `${content}
2282
+
2283
+ ${COURSE_ID_COMMENT_PREFIX} ${courseId} -->`;
2284
+ }
2285
+ function extractCourseId(content) {
2286
+ const match = content.match(/<!-- tostudy-course-id:\s*([a-zA-Z0-9-_]+)\s*-->/);
2287
+ return match?.[1] ?? null;
2288
+ }
2289
+ function inferInstructionTarget(filePath) {
2290
+ if (filePath === ".cursor/rules/tostudy.mdc") {
2291
+ return { ide: "cursor", scenario: "empty" };
2292
+ }
2293
+ if (filePath === ".github/copilot-instructions.md") {
2294
+ return { ide: "copilot", scenario: "empty" };
2295
+ }
2296
+ if (filePath === ".tostudy/AGENTS.md") {
2297
+ return { ide: "codex", scenario: "existing" };
2298
+ }
2299
+ return { ide: "manual", scenario: "empty" };
2300
+ }
2301
+ function resolveDetectedIde(ide) {
2302
+ return ide?.id ?? "manual";
2303
+ }
2304
+
2305
+ // src/commands/init.ts
2306
+ function println4(message = "") {
2307
+ process.stdout.write(`${message}
2308
+ `);
2309
+ }
2310
+ async function runInit(options, dependencies = {}) {
2311
+ const cwd = dependencies.cwd ?? process.cwd();
2312
+ const detectRunningIDE2 = dependencies.detectRunningIDE ?? detectRunningIDE;
2313
+ const detectWorkspaceScenario2 = dependencies.detectWorkspaceScenario ?? detectWorkspaceScenario;
2314
+ const verify = dependencies.verifyHeartbeat ?? verifyHeartbeat;
2315
+ const fetchCourses2 = dependencies.fetchCourses ?? fetchCourses;
2316
+ const fetchCourse = dependencies.fetchCourse ?? fetchCourseContext;
2317
+ const promptForCourse = dependencies.promptForCourse ?? promptCourseSelection;
2318
+ const confirmOverwrite = dependencies.confirmOverwrite ?? confirm;
2319
+ let selectedCourseId = options.courseId;
2320
+ let course = selectedCourseId ? await fetchCourse(selectedCourseId, options.apiKey, options.platformUrl) : null;
2321
+ if (!selectedCourseId) {
2322
+ const availableCourses = await fetchCourses2(options.apiKey, options.platformUrl);
2323
+ if (availableCourses.length === 0) {
2324
+ println4(
2325
+ chalk4.yellow(
2326
+ ' Nenhum curso MCP ativo encontrado. Use "--course <id>" ou configure um curso compat\xEDvel na plataforma.'
2327
+ )
2328
+ );
2329
+ return { ok: false, reason: "course_required" };
2330
+ }
2331
+ if (availableCourses.length === 1) {
2332
+ course = availableCourses[0] ?? null;
2333
+ selectedCourseId = course?.id;
2334
+ println4(chalk4.cyan(` Curso selecionado automaticamente: ${course?.title}`));
2335
+ } else {
2336
+ const promptedCourseId = await promptForCourse(availableCourses);
2337
+ if (!promptedCourseId) {
2338
+ println4(chalk4.yellow(" Sele\xE7\xE3o de curso cancelada."));
2339
+ return { ok: false, reason: "cancelled" };
2340
+ }
2341
+ selectedCourseId = promptedCourseId;
2342
+ course = availableCourses.find((availableCourse) => availableCourse.id === selectedCourseId) ?? null;
2343
+ }
2344
+ }
2345
+ const ide = await detectRunningIDE2();
2346
+ const workspace = await detectWorkspaceScenario2(cwd);
2347
+ const mode = await detectInstructionMode(options.apiKey, options.platformUrl, verify);
2348
+ if (!course) {
2349
+ println4(chalk4.red(` N\xE3o foi poss\xEDvel carregar o curso ${selectedCourseId ?? "selecionado"}.`));
2350
+ return { ok: false, reason: "course_not_found" };
2351
+ }
2352
+ const instructionContent = appendCourseMetadata(
2353
+ generateInstructions(course, {
2354
+ mode,
2355
+ ide: resolveDetectedIde(ide),
2356
+ locale: "pt-BR"
2357
+ }),
2358
+ course.id
2359
+ );
2360
+ const rendered = renderForIDE(instructionContent, resolveDetectedIde(ide), workspace.scenario);
2361
+ const outputPath = join10(cwd, rendered.filePath);
2362
+ if (existsSync13(outputPath)) {
2363
+ const shouldOverwrite = await confirmOverwrite(
2364
+ ` ${rendered.filePath} j\xE1 existe. Sobrescrever?`,
2365
+ true
2366
+ );
2367
+ if (!shouldOverwrite) {
2368
+ println4(chalk4.yellow(" Cancelado pelo usu\xE1rio."));
2369
+ return { ok: false, reason: "cancelled" };
2370
+ }
2371
+ }
2372
+ mkdirSync9(dirname9(outputPath), { recursive: true });
2373
+ writeFileSync9(outputPath, rendered.content, "utf-8");
2374
+ println4(chalk4.green(` \u2713 Workspace configurado em ${rendered.filePath}`));
2375
+ return {
2376
+ ok: true,
2377
+ outputPath,
2378
+ mode,
2379
+ courseId: course.id
2380
+ };
2381
+ }
2382
+
2383
+ // src/commands/sync.ts
2384
+ import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync10 } from "fs";
2385
+ import { dirname as dirname10, join as join11 } from "path";
2386
+ import chalk5 from "chalk";
2387
+ function println5(message = "") {
2388
+ process.stdout.write(`${message}
2389
+ `);
2390
+ }
2391
+ var INSTRUCTION_CANDIDATES = [
2392
+ "AGENTS.md",
2393
+ ".tostudy/AGENTS.md",
2394
+ ".cursor/rules/tostudy.mdc",
2395
+ ".github/copilot-instructions.md"
598
2396
  ];
599
- program
600
- .command("install")
601
- .description("Install MCP config for a specific IDE (used by the web setup wizard)")
602
- .requiredOption("--ide <ide>", `Target IDE: ${SUPPORTED_IDES.join(", ")}`)
603
- .requiredOption("--key <key>", "API key from the platform")
604
- .option("--url <url>", "Platform URL (default: https://tostudy.ai)", DEFAULT_PLATFORM_URL)
605
- .action(async (options) => {
2397
+ function findExistingInstruction(cwd) {
2398
+ for (const candidate of INSTRUCTION_CANDIDATES) {
2399
+ const fullPath = join11(cwd, candidate);
2400
+ if (existsSync14(fullPath)) {
2401
+ return {
2402
+ relativePath: candidate,
2403
+ content: readFileSync9(fullPath, "utf-8")
2404
+ };
2405
+ }
2406
+ }
2407
+ return null;
2408
+ }
2409
+ async function runSync(options, dependencies = {}) {
2410
+ const cwd = dependencies.cwd ?? process.cwd();
2411
+ const verify = dependencies.verifyHeartbeat ?? verifyHeartbeat;
2412
+ const fetchCourse = dependencies.fetchCourse ?? fetchCourseContext;
2413
+ const existing = findExistingInstruction(cwd);
2414
+ if (!existing) {
2415
+ println5(
2416
+ chalk5.yellow(' Nenhum arquivo ToStudy encontrado. Rode "tostudy-mcp-setup init" primeiro.')
2417
+ );
2418
+ return { ok: false, reason: "instruction_missing" };
2419
+ }
2420
+ const courseId = options.courseId ?? extractCourseId(existing.content);
2421
+ if (!courseId) {
2422
+ println5(
2423
+ chalk5.yellow(
2424
+ ' Metadata do curso ausente. Rode "tostudy-mcp-setup init --course <id>" novamente.'
2425
+ )
2426
+ );
2427
+ return { ok: false, reason: "course_metadata_missing" };
2428
+ }
2429
+ const mode = await detectInstructionMode(options.apiKey, options.platformUrl, verify);
2430
+ const course = await fetchCourse(courseId, options.apiKey, options.platformUrl);
2431
+ if (!course) {
2432
+ println5(chalk5.red(` N\xE3o foi poss\xEDvel sincronizar o curso ${courseId}.`));
2433
+ return { ok: false, reason: "course_not_found" };
2434
+ }
2435
+ const target = inferInstructionTarget(existing.relativePath);
2436
+ const instructionContent = appendCourseMetadata(
2437
+ generateInstructions(course, {
2438
+ mode,
2439
+ ide: target.ide,
2440
+ locale: "pt-BR"
2441
+ }),
2442
+ course.id
2443
+ );
2444
+ const rendered = renderForIDE(instructionContent, target.ide, target.scenario);
2445
+ const outputPath = join11(cwd, rendered.filePath);
2446
+ mkdirSync10(dirname10(outputPath), { recursive: true });
2447
+ writeFileSync10(outputPath, rendered.content, "utf-8");
2448
+ println5(chalk5.green(` \u2713 ${rendered.filePath} sincronizado`));
2449
+ return {
2450
+ ok: true,
2451
+ outputPath,
2452
+ courseId: course.id,
2453
+ mode
2454
+ };
2455
+ }
2456
+
2457
+ // src/index.ts
2458
+ var VERSION = "1.3.0";
2459
+ var DEFAULT_PLATFORM_URL2 = "https://tostudy.ai";
2460
+ var OAUTH_PORT = 9877;
2461
+ function println6(message = "") {
2462
+ process.stdout.write(`${message}
2463
+ `);
2464
+ }
2465
+ function eprintln(message = "") {
2466
+ process.stderr.write(`${message}
2467
+ `);
2468
+ }
2469
+ function printBanner() {
2470
+ println6();
2471
+ println6(chalk6.cyan(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
2472
+ println6(
2473
+ chalk6.cyan(" \u2551") + chalk6.white.bold(" ToStudy MCP Setup ") + chalk6.cyan("\u2551")
2474
+ );
2475
+ println6(chalk6.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
2476
+ println6();
2477
+ }
2478
+ async function validateApiKey(apiKey, platformUrl) {
2479
+ try {
2480
+ const response = await fetch(`${platformUrl}/api/mcp/heartbeat`, {
2481
+ method: "POST",
2482
+ headers: {
2483
+ Authorization: `Bearer ${apiKey}`,
2484
+ "Content-Type": "application/json"
2485
+ },
2486
+ body: JSON.stringify({ timestamp: (/* @__PURE__ */ new Date()).toISOString() })
2487
+ });
2488
+ return response.ok || response.status === 204;
2489
+ } catch {
2490
+ println6(chalk6.yellow("! N\xE3o foi poss\xEDvel validar a API key (servidor offline?)"));
2491
+ println6(chalk6.yellow(" A configura\xE7\xE3o ser\xE1 salva mesmo assim."));
2492
+ return true;
2493
+ }
2494
+ }
2495
+ async function exchangeCodeForToken(code, platformUrl, client = "mcp") {
2496
+ const res = await fetch(`${platformUrl}/api/cli/auth/exchange`, {
2497
+ method: "POST",
2498
+ headers: { "Content-Type": "application/json" },
2499
+ body: JSON.stringify({ code, client })
2500
+ });
2501
+ if (!res.ok) {
2502
+ const body = await res.json().catch(() => ({}));
2503
+ throw new Error(body.error ?? `Falha na autentica\xE7\xE3o (${res.status})`);
2504
+ }
2505
+ return res.json();
2506
+ }
2507
+ async function configureIDEs(token, platformUrl) {
2508
+ const mcpUrl = resolveMcpServerUrl(platformUrl);
2509
+ const allDetected = await detectInstalledIDEs();
2510
+ const installedIDEs = allDetected.filter((h) => h.id !== "manual");
2511
+ if (installedIDEs.length === 0) {
2512
+ println6(chalk6.yellow(" Nenhuma IDE suportada detectada."));
2513
+ println6(chalk6.gray(" Use: npx @tostudy-ai/mcp-setup install --ide <nome> --key <token>"));
2514
+ return 0;
2515
+ }
2516
+ println6(chalk6.white(" IDEs detectadas:\n"));
2517
+ for (let i = 0; i < installedIDEs.length; i++) {
2518
+ println6(` ${chalk6.cyan(String(i + 1))}. ${installedIDEs[i].name}`);
2519
+ }
2520
+ println6(` ${chalk6.cyan("a")}. Todas`);
2521
+ println6();
2522
+ const answer = await prompt(
2523
+ " Quais configurar? (n\xFAmeros separados por v\xEDrgula, ou 'a' para todas) "
2524
+ );
2525
+ const trimmed = answer.trim().toLowerCase();
2526
+ let selectedIDEs;
2527
+ if (!trimmed || trimmed === "a" || trimmed === "all") {
2528
+ selectedIDEs = installedIDEs;
2529
+ } else {
2530
+ const indices = trimmed.split(/[,\s]+/).map((s) => parseInt(s, 10) - 1).filter((i) => i >= 0 && i < installedIDEs.length);
2531
+ if (indices.length === 0) {
2532
+ println6(chalk6.yellow(" Nenhuma IDE selecionada."));
2533
+ return 0;
2534
+ }
2535
+ selectedIDEs = indices.map((i) => installedIDEs[i]);
2536
+ }
2537
+ println6();
2538
+ const claudeCodeHandler = selectedIDEs.find((h) => h.id === "claude-code");
2539
+ let claudeCodeScope = "user";
2540
+ if (claudeCodeHandler) {
2541
+ claudeCodeScope = await promptClaudeCodeScope();
2542
+ println6();
2543
+ }
2544
+ let configured = 0;
2545
+ for (const handler of selectedIDEs) {
2546
+ process.stdout.write(` ${chalk6.gray("Configurando")} ${handler.name}... `);
606
2547
  try {
607
- const ide = options.ide;
608
- if (!SUPPORTED_IDES.includes(ide)) {
609
- eprintln(chalk.red(`Unknown IDE: ${ide}`));
610
- eprintln(`Supported: ${SUPPORTED_IDES.join(", ")}`);
611
- process.exit(1);
612
- }
613
- const handler = getIDEHandler(ide);
614
- const mcpUrl = resolveMcpServerUrl(options.url);
615
- println();
616
- println(chalk.cyan(` Installing MCP config for ${handler.name}...`));
617
- // Write config
618
- process.stdout.write(chalk.gray(" Writing config... "));
619
- await handler.writeConfig(options.key, mcpUrl);
620
- println(chalk.green("OK"));
621
- if (ide !== "manual") {
622
- println(chalk.gray(` Config: ${handler.getConfigPath()}`));
623
- }
624
- // Verify heartbeat
625
- process.stdout.write(chalk.gray(" Verifying connection... "));
626
- const verified = await handler.verify(options.key, options.url);
627
- if (verified) {
628
- println(chalk.green("OK"));
629
- }
630
- else {
631
- println(chalk.yellow("SKIPPED (server not reachable)"));
632
- println(chalk.gray(" The config was saved. Connection will work when the server is available."));
2548
+ if (handler.id === "claude-code") {
2549
+ await writeClaudeCodeConfig(handler, token, mcpUrl, claudeCodeScope);
2550
+ } else {
2551
+ await handler.writeConfig(token, mcpUrl);
2552
+ }
2553
+ println6(chalk6.green("OK"));
2554
+ configured++;
2555
+ } catch (error) {
2556
+ println6(chalk6.red("FALHOU"));
2557
+ eprintln(chalk6.gray(` ${error instanceof Error ? error.message : String(error)}`));
2558
+ }
2559
+ }
2560
+ return configured;
2561
+ }
2562
+ async function writeClaudeCodeConfig(_handler, token, mcpUrl, scope) {
2563
+ const { execFileSync: execFileSync5 } = await import("child_process");
2564
+ const addArgs = buildClaudeCodeAddArgs(token, mcpUrl, scope);
2565
+ const removeArgs = buildClaudeCodeRemoveArgs(scope);
2566
+ try {
2567
+ execFileSync5("claude", removeArgs, { stdio: "ignore" });
2568
+ } catch {
2569
+ }
2570
+ execFileSync5("claude", addArgs, { stdio: "inherit" });
2571
+ }
2572
+ function printSuccessBanner(configuredCount) {
2573
+ println6();
2574
+ if (configuredCount > 0) {
2575
+ println6(chalk6.green(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
2576
+ println6(
2577
+ chalk6.green(" \u2551") + chalk6.white.bold(
2578
+ ` ${configuredCount} IDE${configuredCount > 1 ? "s" : ""} configurada${configuredCount > 1 ? "s" : ""} com sucesso!`.padEnd(
2579
+ 42
2580
+ )
2581
+ ) + chalk6.green("\u2551")
2582
+ );
2583
+ println6(chalk6.green(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
2584
+ } else {
2585
+ println6(chalk6.yellow(" Nenhuma IDE foi configurada."));
2586
+ println6(chalk6.gray(" Use: npx @tostudy-ai/mcp-setup install --ide <nome> --key <token>"));
2587
+ }
2588
+ println6();
2589
+ println6(chalk6.white(NEXT_STEPS_TITLE));
2590
+ println6(chalk6.gray(" 1.") + " Reinicie as IDEs configuradas");
2591
+ println6(chalk6.gray(" 2.") + ASSISTANT_COURSES_PROMPT.trimStart());
2592
+ println6();
2593
+ println6(chalk6.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2594
+ println6();
2595
+ println6(chalk6.white(" Prefere estudar pelo terminal?"));
2596
+ println6(chalk6.gray(" ") + chalk6.cyan("npm install -g @tostudy-ai/cli"));
2597
+ println6(
2598
+ chalk6.gray(" ") + chalk6.cyan("tostudy login") + chalk6.gray(" \u2192 ") + chalk6.cyan("tostudy courses")
2599
+ );
2600
+ println6();
2601
+ }
2602
+ async function setup(apiKey, platformUrl) {
2603
+ printBanner();
2604
+ const url = platformUrl || process.env.TOSTUDY_PLATFORM_URL || DEFAULT_PLATFORM_URL2;
2605
+ if (apiKey || process.env.TOSTUDY_API_KEY) {
2606
+ const key = apiKey || process.env.TOSTUDY_API_KEY;
2607
+ println6(chalk6.yellow(` ${API_KEY_DEPRECATION_WARNING}`));
2608
+ println6();
2609
+ println6(chalk6.gray(" Usando API key fornecida..."));
2610
+ process.stdout.write(chalk6.gray(" Validando... "));
2611
+ const valid = await validateApiKey(key, url);
2612
+ if (!valid) {
2613
+ println6(chalk6.red("FALHOU"));
2614
+ println6(chalk6.red(" API key inv\xE1lida ou expirada."));
2615
+ process.exit(1);
2616
+ }
2617
+ println6(chalk6.green("OK"));
2618
+ println6();
2619
+ const configured2 = await configureIDEs(key, url);
2620
+ printSuccessBanner(configured2);
2621
+ return;
2622
+ }
2623
+ println6(chalk6.gray(" Abrindo browser para autentica\xE7\xE3o...\n"));
2624
+ let code;
2625
+ try {
2626
+ const serverPromise = startCallbackServer(OAUTH_PORT);
2627
+ const authUrl = `${url}/api/cli/auth/authorize?port=${OAUTH_PORT}`;
2628
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
2629
+ const openArgs = process.platform === "win32" ? ["", authUrl] : [authUrl];
2630
+ execFile(openCmd, openArgs, (err) => {
2631
+ if (err) {
2632
+ println6(chalk6.yellow(" N\xE3o foi poss\xEDvel abrir o browser automaticamente."));
2633
+ println6(chalk6.gray(` Abra manualmente: ${authUrl}
2634
+ `));
2635
+ }
2636
+ });
2637
+ const result = await serverPromise;
2638
+ code = result.code;
2639
+ } catch (err) {
2640
+ const msg = err instanceof Error ? err.message : String(err);
2641
+ eprintln(chalk6.red(` Erro: ${msg}`));
2642
+ process.exit(1);
2643
+ }
2644
+ process.stdout.write(chalk6.gray(" Autenticando... "));
2645
+ let token;
2646
+ let userName;
2647
+ try {
2648
+ const result = await exchangeCodeForToken(code, url, "mcp");
2649
+ token = result.token;
2650
+ userName = result.userName;
2651
+ println6(chalk6.green("OK"));
2652
+ println6(chalk6.green(` Logado como ${userName}
2653
+ `));
2654
+ } catch (err) {
2655
+ println6(chalk6.red("FALHOU"));
2656
+ eprintln(chalk6.red(` ${err instanceof Error ? err.message : String(err)}`));
2657
+ process.exit(1);
2658
+ }
2659
+ println6(chalk6.gray(" Detectando IDEs...\n"));
2660
+ const configured = await configureIDEs(token, url);
2661
+ printSuccessBanner(configured);
2662
+ }
2663
+ async function uninstall() {
2664
+ printBanner();
2665
+ if (!isTostudyMcpConfigured()) {
2666
+ println6(chalk6.yellow("ToStudy MCP n\xE3o est\xE1 configurado."));
2667
+ process.exit(0);
2668
+ }
2669
+ const shouldUninstall = await confirm("Remover configura\xE7\xE3o do ToStudy MCP?", false);
2670
+ if (!shouldUninstall) {
2671
+ println6(chalk6.gray("Opera\xE7\xE3o cancelada."));
2672
+ process.exit(0);
2673
+ }
2674
+ process.stdout.write(chalk6.gray("Removendo configura\xE7\xE3o... "));
2675
+ try {
2676
+ removeTostudyMcpServer();
2677
+ println6(chalk6.green("OK"));
2678
+ println6();
2679
+ println6(chalk6.green("Configura\xE7\xE3o removida com sucesso."));
2680
+ println6(chalk6.gray("Reinicie o Claude Code para aplicar as mudan\xE7as."));
2681
+ println6();
2682
+ } catch (error) {
2683
+ println6(chalk6.red("FALHOU"));
2684
+ eprintln(error instanceof Error ? error.message : String(error));
2685
+ process.exit(1);
2686
+ }
2687
+ }
2688
+ function printStep(step, total, title) {
2689
+ println6();
2690
+ println6(chalk6.cyan(`\u2501\u2501\u2501 Passo ${step}/${total}: ${title} \u2501\u2501\u2501`));
2691
+ println6();
2692
+ }
2693
+ function printIDEDetection(installedIDEs) {
2694
+ if (installedIDEs.length === 0) {
2695
+ println6(chalk6.yellow(" Nenhuma IDE suportada detectada."));
2696
+ println6(chalk6.gray(" O setup continuar\xE1 para Claude Code."));
2697
+ } else {
2698
+ println6(chalk6.green(" IDEs detectadas:"));
2699
+ for (const ide of installedIDEs) {
2700
+ const version = ide.version ? chalk6.gray(` (${ide.version})`) : "";
2701
+ println6(` ${chalk6.green("\u2713")} ${ide.name}${version}`);
2702
+ }
2703
+ }
2704
+ }
2705
+ async function wizard(options) {
2706
+ const TOTAL_STEPS = 4;
2707
+ println6();
2708
+ println6(chalk6.cyan(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
2709
+ println6(
2710
+ chalk6.cyan(" \u2551") + chalk6.white.bold(" ToStudy MCP Setup Wizard ") + chalk6.cyan("\u2551")
2711
+ );
2712
+ println6(
2713
+ chalk6.cyan(" \u2551") + chalk6.gray(" Configura\xE7\xE3o guiada passo a passo ") + chalk6.cyan("\u2551")
2714
+ );
2715
+ println6(chalk6.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
2716
+ println6();
2717
+ println6(chalk6.gray(" Este assistente vai configurar o Claude Code para"));
2718
+ println6(chalk6.gray(" conectar ao servidor MCP da plataforma ToStudy."));
2719
+ println6();
2720
+ printStep(1, TOTAL_STEPS, "Detectando ambiente");
2721
+ process.stdout.write(" Verificando Claude Code... ");
2722
+ if (!isClaudeInstalled()) {
2723
+ println6(chalk6.red("N\xC3O ENCONTRADO"));
2724
+ println6();
2725
+ println6(chalk6.red(" Claude Code n\xE3o est\xE1 instalado."));
2726
+ println6();
2727
+ println6(" Por favor, instale primeiro:");
2728
+ println6(chalk6.cyan(" https://claude.ai/download"));
2729
+ println6();
2730
+ process.exit(1);
2731
+ }
2732
+ println6(chalk6.green("OK"));
2733
+ const configPath = getClaudeConfigPath();
2734
+ println6(chalk6.gray(` Config: ${configPath}`));
2735
+ process.stdout.write(" Detectando IDEs... ");
2736
+ const installedIDEs = getInstalledIDEs();
2737
+ println6(chalk6.green("OK"));
2738
+ printIDEDetection(installedIDEs);
2739
+ process.stdout.write(" Verificando configura\xE7\xE3o atual... ");
2740
+ const alreadyConfigured = isTostudyMcpConfigured();
2741
+ if (alreadyConfigured) {
2742
+ println6(chalk6.yellow("J\xC1 CONFIGURADO"));
2743
+ println6();
2744
+ const overwrite = await confirm(" Deseja reconfigurar?", false);
2745
+ if (!overwrite) {
2746
+ println6();
2747
+ println6(chalk6.gray(" Opera\xE7\xE3o cancelada."));
2748
+ process.exit(0);
2749
+ }
2750
+ } else {
2751
+ println6(chalk6.gray("N\xE3o configurado"));
2752
+ }
2753
+ printStep(2, TOTAL_STEPS, "Configurando API Key");
2754
+ println6(chalk6.gray(" A API key conecta o Claude Code a sua conta na plataforma."));
2755
+ println6();
2756
+ let apiKey = options.apiKey || process.env.TOSTUDY_API_KEY;
2757
+ if (!apiKey) {
2758
+ println6(" Acesse sua API key em:");
2759
+ println6(chalk6.cyan(` ${options.url || DEFAULT_PLATFORM_URL2}/student/settings/mcp`));
2760
+ println6();
2761
+ apiKey = await promptApiKey();
2762
+ } else {
2763
+ println6(chalk6.yellow(` ${API_KEY_DEPRECATION_WARNING}`));
2764
+ println6();
2765
+ println6(chalk6.green(" \u2713 API key fornecida via par\xE2metro ou ambiente"));
2766
+ }
2767
+ const platformUrl = options.url || process.env.TOSTUDY_PLATFORM_URL || DEFAULT_PLATFORM_URL2;
2768
+ println6();
2769
+ process.stdout.write(chalk6.gray(" Validando API key... "));
2770
+ const valid = await validateApiKey(apiKey, platformUrl);
2771
+ if (!valid) {
2772
+ println6(chalk6.red("FALHOU"));
2773
+ println6();
2774
+ println6(chalk6.red(" API key inv\xE1lida ou expirada."));
2775
+ println6();
2776
+ println6(" Para gerar uma nova API key:");
2777
+ println6(chalk6.cyan(` ${platformUrl}/student/settings/mcp`));
2778
+ println6();
2779
+ process.exit(1);
2780
+ }
2781
+ println6(chalk6.green("OK"));
2782
+ printStep(3, TOTAL_STEPS, "Salvando configura\xE7\xE3o");
2783
+ process.stdout.write(" Configurando Claude Code... ");
2784
+ try {
2785
+ addTostudyMcpServer(apiKey, platformUrl);
2786
+ println6(chalk6.green("OK"));
2787
+ } catch (error) {
2788
+ println6(chalk6.red("FALHOU"));
2789
+ eprintln();
2790
+ eprintln(chalk6.red(" Erro ao salvar configura\xE7\xE3o:"));
2791
+ eprintln(" " + (error instanceof Error ? error.message : String(error)));
2792
+ process.exit(1);
2793
+ }
2794
+ printStep(4, TOTAL_STEPS, "Verifica\xE7\xE3o final");
2795
+ if (!options.skipDiagnostics) {
2796
+ process.stdout.write(" Executando diagn\xF3stico... ");
2797
+ const report = await runDiagnostics();
2798
+ println6(chalk6.green("OK"));
2799
+ println6();
2800
+ const remainingIssues = report.issues.filter(
2801
+ (issue) => !["mcp-not-configured", "config-missing"].includes(issue.id)
2802
+ );
2803
+ if (remainingIssues.length > 0) {
2804
+ println6(chalk6.yellow(" Avisos encontrados:"));
2805
+ for (const issue of remainingIssues) {
2806
+ const icon = issue.severity === "critical" ? chalk6.red("\u25CF") : issue.severity === "warning" ? chalk6.yellow("\u25CF") : chalk6.blue("\u25CF");
2807
+ println6(` ${icon} ${issue.title}`);
2808
+ }
2809
+ println6();
2810
+ if (options.autoRepair) {
2811
+ process.stdout.write(" Tentando reparar automaticamente... ");
2812
+ const repairReport = repairAllIssues(report, apiKey, platformUrl);
2813
+ if (repairReport.repairsSucceeded > 0) {
2814
+ println6(chalk6.green(`${repairReport.repairsSucceeded} corrigido(s)`));
2815
+ } else {
2816
+ println6(chalk6.yellow("Nenhum reparo aplicado"));
633
2817
  }
634
- println();
635
- println(chalk.green.bold(" Done! Restart your IDE to activate the MCP server."));
636
- println();
2818
+ } else {
2819
+ println6(
2820
+ chalk6.gray(' Dica: Execute "npx @tostudy-ai/mcp-setup diagnose" para mais detalhes.')
2821
+ );
2822
+ }
2823
+ } else {
2824
+ println6(chalk6.green(" \u2713 Nenhum problema encontrado!"));
637
2825
  }
638
- catch (error) {
639
- eprintln();
640
- eprintln(chalk.red("Install failed:") +
641
- " " +
642
- (error instanceof Error ? error.message : String(error)));
643
- process.exit(1);
2826
+ } else {
2827
+ println6(chalk6.gray(" Diagn\xF3stico ignorado (--skip-diagnostics)"));
2828
+ }
2829
+ println6();
2830
+ println6(chalk6.green(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
2831
+ println6(
2832
+ chalk6.green(" \u2551") + chalk6.white.bold(" Configura\xE7\xE3o conclu\xEDda com sucesso! ") + chalk6.green("\u2551")
2833
+ );
2834
+ println6(chalk6.green(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
2835
+ println6();
2836
+ println6(chalk6.white(NEXT_STEPS_TITLE));
2837
+ println6();
2838
+ println6(chalk6.gray(" 1.") + " Reinicie o Claude Code");
2839
+ println6(chalk6.gray(" 2.") + " O servidor MCP iniciar\xE1 automaticamente");
2840
+ println6(chalk6.gray(" 3.") + COURSES_TOOL_HINT.trimStart());
2841
+ println6();
2842
+ println6(chalk6.gray(" Comandos \xFAteis:"));
2843
+ println6(
2844
+ chalk6.gray(" ") + chalk6.cyan("npx @tostudy-ai/mcp-setup diagnose") + chalk6.gray(" - Verificar problemas")
2845
+ );
2846
+ println6(
2847
+ chalk6.gray(" ") + chalk6.cyan("npx @tostudy-ai/mcp-setup repair") + chalk6.gray(" - Reparar automaticamente")
2848
+ );
2849
+ println6();
2850
+ println6(chalk6.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2851
+ println6();
2852
+ println6(chalk6.white(" Prefere estudar pelo terminal?"));
2853
+ println6(chalk6.gray(" ") + chalk6.cyan("npm install -g @tostudy-ai/cli"));
2854
+ println6(
2855
+ chalk6.gray(" ") + chalk6.cyan("tostudy login") + chalk6.gray(" \u2192 ") + chalk6.cyan("tostudy courses")
2856
+ );
2857
+ println6();
2858
+ }
2859
+ program.name("tostudy-mcp-setup").description("Configura o Claude Code para usar o ToStudy MCP server").version(VERSION);
2860
+ program.option("-k, --api-key <key>", "(deprecated) API key \u2014 use OAuth flow instead").option("-u, --url <url>", "URL da plataforma (default: https://tostudy.ai)").option("--uninstall", "Remove a configura\xE7\xE3o do ToStudy MCP").action(async (options) => {
2861
+ try {
2862
+ if (options.uninstall) {
2863
+ await uninstall();
2864
+ } else {
2865
+ await setup(options.apiKey, options.url);
644
2866
  }
2867
+ } catch (error) {
2868
+ eprintln();
2869
+ eprintln(chalk6.red("Erro:") + " " + (error instanceof Error ? error.message : String(error)));
2870
+ process.exit(1);
2871
+ }
645
2872
  });
646
- /**
647
- * Resolve the MCP server URL from the platform URL.
648
- * The platform runs on port 3700, MCP server on 3701.
649
- */
650
- function resolveMcpServerUrl(platformUrl) {
651
- if (platformUrl.includes(":3700")) {
652
- return platformUrl.replace(":3700", ":3701");
2873
+ program.command("wizard").description("Assistente interativo de configura\xE7\xE3o passo a passo").option("-k, --api-key <key>", "(deprecated) API key \u2014 use OAuth flow instead").option("-u, --url <url>", "URL da plataforma (default: https://tostudy.ai)").option("--skip-diagnostics", "Pula a verifica\xE7\xE3o de diagn\xF3stico ap\xF3s setup").option("--auto-repair", "Tenta reparar problemas automaticamente").action(async (options) => {
2874
+ try {
2875
+ await wizard({
2876
+ apiKey: options.apiKey,
2877
+ url: options.url,
2878
+ skipDiagnostics: options.skipDiagnostics,
2879
+ autoRepair: options.autoRepair
2880
+ });
2881
+ } catch (error) {
2882
+ eprintln();
2883
+ eprintln(chalk6.red("Erro:") + " " + (error instanceof Error ? error.message : String(error)));
2884
+ process.exit(1);
2885
+ }
2886
+ });
2887
+ program.command("diagnose").description("Executa diagn\xF3stico de problemas na configura\xE7\xE3o MCP").option("--json", "Sa\xEDda em formato JSON").action(async (options) => {
2888
+ try {
2889
+ const report = await runDiagnostics();
2890
+ if (options.json) {
2891
+ println6(JSON.stringify(report, null, 2));
2892
+ } else {
2893
+ printDiagnosticReport(report);
653
2894
  }
654
- if (platformUrl.includes("localhost") && !platformUrl.includes(":")) {
655
- return "http://localhost:3701";
2895
+ process.exit(report.passed ? 0 : 1);
2896
+ } catch (error) {
2897
+ eprintln();
2898
+ eprintln(chalk6.red("Erro ao executar diagn\xF3stico:"));
2899
+ eprintln(error instanceof Error ? error.message : String(error));
2900
+ process.exit(1);
2901
+ }
2902
+ });
2903
+ program.command("repair").description("Repara problemas de configura\xE7\xE3o automaticamente").option("-k, --api-key <key>", "API key para reparos que necessitam autentica\xE7\xE3o").option("-u, --url <url>", "URL da plataforma (default: https://tostudy.ai)").option("--json", "Sa\xEDda em formato JSON").action(async (options) => {
2904
+ try {
2905
+ const apiKey = options.apiKey || process.env.TOSTUDY_API_KEY;
2906
+ const platformUrl = options.url || DEFAULT_PLATFORM_URL2;
2907
+ const diagnostic = await runDiagnostics();
2908
+ const repair = repairAllIssues(diagnostic, apiKey, platformUrl);
2909
+ if (options.json) {
2910
+ println6(JSON.stringify({ diagnostic, repair }, null, 2));
2911
+ } else {
2912
+ printRepairReport(repair);
2913
+ }
2914
+ const hasUnfixedCritical = diagnostic.issues.some(
2915
+ (issue) => issue.severity === "critical" && !repair.results.find((r) => r.issueId === issue.id && r.success)
2916
+ );
2917
+ process.exit(hasUnfixedCritical ? 1 : 0);
2918
+ } catch (error) {
2919
+ eprintln();
2920
+ eprintln(chalk6.red("Erro ao executar reparo:"));
2921
+ eprintln(error instanceof Error ? error.message : String(error));
2922
+ process.exit(1);
2923
+ }
2924
+ });
2925
+ var SUPPORTED_IDES = [
2926
+ "claude-code",
2927
+ "cursor",
2928
+ "vscode",
2929
+ "desktop",
2930
+ "windsurf",
2931
+ "opencode",
2932
+ "codex",
2933
+ "antigravity",
2934
+ "manual"
2935
+ ];
2936
+ program.command("install").description("Install MCP config for a specific IDE (used by the web setup wizard)").requiredOption("--ide <ide>", `Target IDE: ${SUPPORTED_IDES.join(", ")}`).requiredOption("--key <key>", "API key from the platform").option("--url <url>", "Platform URL (default: https://tostudy.ai)", DEFAULT_PLATFORM_URL2).action(async (options) => {
2937
+ try {
2938
+ const ide = options.ide;
2939
+ if (!SUPPORTED_IDES.includes(ide)) {
2940
+ eprintln(chalk6.red(`Unknown IDE: ${ide}`));
2941
+ eprintln(`Supported: ${SUPPORTED_IDES.join(", ")}`);
2942
+ process.exit(1);
656
2943
  }
657
- // Production: use dedicated MCP subdomain
658
- if (platformUrl.includes("tostudy")) {
659
- return process.env.MCP_SERVER_URL || "https://mcp.tostudy.ai";
2944
+ const handler = getIDEHandler(ide);
2945
+ const mcpUrl = resolveMcpServerUrl(options.url);
2946
+ println6();
2947
+ println6(chalk6.cyan(` Installing MCP config for ${handler.name}...`));
2948
+ process.stdout.write(chalk6.gray(" Writing config... "));
2949
+ await handler.writeConfig(options.key, mcpUrl);
2950
+ println6(chalk6.green("OK"));
2951
+ if (ide !== "manual") {
2952
+ println6(chalk6.gray(` Config: ${handler.getConfigPath()}`));
660
2953
  }
661
- return platformUrl;
2954
+ process.stdout.write(chalk6.gray(" Verifying connection... "));
2955
+ const verified = await handler.verify(options.key, options.url);
2956
+ if (verified) {
2957
+ println6(chalk6.green("OK"));
2958
+ } else {
2959
+ println6(chalk6.yellow("SKIPPED (server not reachable)"));
2960
+ println6(
2961
+ chalk6.gray(" The config was saved. Connection will work when the server is available.")
2962
+ );
2963
+ }
2964
+ println6();
2965
+ println6(chalk6.green.bold(" Done! Restart your IDE to activate the MCP server."));
2966
+ println6();
2967
+ } catch (error) {
2968
+ eprintln();
2969
+ eprintln(
2970
+ chalk6.red("Install failed:") + " " + (error instanceof Error ? error.message : String(error))
2971
+ );
2972
+ process.exit(1);
2973
+ }
2974
+ });
2975
+ function resolveMcpServerUrl(platformUrl) {
2976
+ if (platformUrl.includes(":3700")) {
2977
+ return platformUrl.replace(":3700", ":3701");
2978
+ }
2979
+ if (platformUrl.includes("localhost") && !platformUrl.includes(":")) {
2980
+ return "http://localhost:3701";
2981
+ }
2982
+ if (platformUrl.includes("tostudy")) {
2983
+ return process.env.MCP_SERVER_URL || "https://mcp.tostudy.ai";
2984
+ }
2985
+ return platformUrl;
2986
+ }
2987
+ function resolvePlatformUrl(explicitUrl) {
2988
+ return explicitUrl || process.env.TOSTUDY_PLATFORM_URL || DEFAULT_PLATFORM_URL2;
662
2989
  }
2990
+ async function resolveCommandApiKey(explicitApiKey) {
2991
+ const apiKey = explicitApiKey || process.env.TOSTUDY_API_KEY;
2992
+ if (apiKey) {
2993
+ return apiKey;
2994
+ }
2995
+ return promptApiKey();
2996
+ }
2997
+ program.command("init").description("Configura instru\xE7\xF5es do curso no workspace atual").requiredOption("--course <courseId>", "Course ID to configure in this workspace").option("-k, --api-key <key>", "API key para autentica\xE7\xE3o").option("-u, --url <url>", "URL da plataforma (default: https://tostudy.ai)").action(async (options) => {
2998
+ try {
2999
+ const result = await runInit({
3000
+ apiKey: await resolveCommandApiKey(options.apiKey),
3001
+ platformUrl: resolvePlatformUrl(options.url),
3002
+ courseId: options.course
3003
+ });
3004
+ if (!result.ok) {
3005
+ process.exit(1);
3006
+ }
3007
+ } catch (error) {
3008
+ eprintln();
3009
+ eprintln(
3010
+ chalk6.red("Erro no init:") + " " + (error instanceof Error ? error.message : String(error))
3011
+ );
3012
+ process.exit(1);
3013
+ }
3014
+ });
3015
+ program.command("sync").description("Regenera as instru\xE7\xF5es do curso com base no arquivo existente").option("--course <courseId>", "Course ID override when metadata is unavailable").option("-k, --api-key <key>", "API key para autentica\xE7\xE3o").option("-u, --url <url>", "URL da plataforma (default: https://tostudy.ai)").action(async (options) => {
3016
+ try {
3017
+ const result = await runSync({
3018
+ apiKey: await resolveCommandApiKey(options.apiKey),
3019
+ platformUrl: resolvePlatformUrl(options.url),
3020
+ courseId: options.course
3021
+ });
3022
+ if (!result.ok) {
3023
+ process.exit(1);
3024
+ }
3025
+ } catch (error) {
3026
+ eprintln();
3027
+ eprintln(
3028
+ chalk6.red("Erro no sync:") + " " + (error instanceof Error ? error.message : String(error))
3029
+ );
3030
+ process.exit(1);
3031
+ }
3032
+ });
663
3033
  program.parse();
664
3034
  //# sourceMappingURL=index.js.map