devto-mcp 0.1.9 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7,13 +7,12 @@
7
7
  * devto login Authenticate with your DevTo API key
8
8
  * devto logout Clear global credentials
9
9
  * devto status Show current connection status
10
- * devto init Auto-configure Claude Code MCP config
10
+ * devto init Link this project to DevTo
11
+ * devto unlink Unlink this project from DevTo
11
12
  * devto doctor Test full connection chain
12
13
  * devto sync Re-sync workspace configuration
13
14
  * devto verbose Toggle verbose mode
14
15
  * devto config set anthropic-key <key> Store Anthropic API key
15
- * devto uninstall Remove DevTo from this project
16
- * devto uninstall --purge Remove DevTo from this project + global config
17
16
  */
18
17
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
18
  if (k2 === undefined) k2 = k;
@@ -55,6 +54,19 @@ const path = __importStar(require("path"));
55
54
  const os = __importStar(require("os"));
56
55
  const config_1 = require("./config");
57
56
  const command = process.argv[2];
57
+ const NO_PROJECT_MESSAGE = `\nNo project linked in this directory.\nRun \`devto init\` to link this project to a DevTo workspace.\n`;
58
+ /**
59
+ * Require .devto.json in the directory tree.
60
+ * If not found, print message and exit. Used by status, doctor, sync.
61
+ */
62
+ function requireProjectJson() {
63
+ const projectJson = (0, config_1.findProjectJson)();
64
+ if (!projectJson) {
65
+ console.log(NO_PROJECT_MESSAGE);
66
+ process.exit(1);
67
+ }
68
+ return projectJson;
69
+ }
58
70
  async function prompt(question, hidden = false) {
59
71
  return new Promise((resolve) => {
60
72
  const rl = readline.createInterface({
@@ -99,7 +111,7 @@ async function validateKey(apiKey) {
99
111
  const res = await fetch(`${apiUrl}/api/v1/status`, {
100
112
  headers: {
101
113
  Authorization: `Bearer ${apiKey}`,
102
- "X-DevTo-Version": "0.1.9",
114
+ "X-DevTo-Version": config_1.VERSION,
103
115
  },
104
116
  });
105
117
  // 200 = valid key, 401 = invalid
@@ -110,8 +122,38 @@ async function validateKey(apiKey) {
110
122
  return true;
111
123
  }
112
124
  }
125
+ /**
126
+ * Fetch workspaces for a given API key.
127
+ * Returns array of { id, name, workspaceUrl, projectKey, providerType }.
128
+ */
129
+ async function fetchWorkspaces(apiKey) {
130
+ const apiUrl = process.env.DEVTO_API_URL ?? "https://api.devto.ai";
131
+ try {
132
+ const res = await fetch(`${apiUrl}/api/v1/workspaces`, {
133
+ headers: {
134
+ Authorization: `Bearer ${apiKey}`,
135
+ "X-DevTo-Version": config_1.VERSION,
136
+ },
137
+ });
138
+ if (!res.ok)
139
+ return [];
140
+ const data = (await res.json());
141
+ return (data.workspaces ?? []).map((w) => ({
142
+ id: w.id,
143
+ name: w.name,
144
+ workspaceUrl: w.workspace_url,
145
+ projectKey: w.workspace_project_key,
146
+ providerType: w.provider_type,
147
+ }));
148
+ }
149
+ catch {
150
+ return [];
151
+ }
152
+ }
113
153
  async function login() {
114
- console.log("\nDevTo Login\n");
154
+ console.log("\nDevTo Login (global)\n");
155
+ console.log("Note: For per-project isolation, use `devto init` instead.");
156
+ console.log(" `devto login` sets a global fallback key.\n");
115
157
  console.log("Get your API key from: https://devto.ai/dashboard/keys\n");
116
158
  const apiKey = await prompt("Paste your API key: ", true);
117
159
  if (!apiKey || !apiKey.startsWith("devto_")) {
@@ -148,7 +190,7 @@ async function login() {
148
190
  else {
149
191
  console.log("\nSkipped. You can set it later: devto config set anthropic-key sk-ant-xxxx\n");
150
192
  }
151
- console.log("Next step: run `devto init` to configure Claude Code MCP.");
193
+ console.log("Next step: run `devto init` in your project directory.");
152
194
  console.log("Run `devto status` to verify your connection.\n");
153
195
  }
154
196
  async function logout() {
@@ -156,39 +198,61 @@ async function logout() {
156
198
  if (fs.existsSync(configDir)) {
157
199
  fs.rmSync(configDir, { recursive: true, force: true });
158
200
  console.log("\nLogged out. Removed ~/.devto credentials.");
159
- console.log("Note: Project .mcp.json files still have embedded keys MCP servers will keep working.");
160
- console.log("Run `devto uninstall` in a project to remove it from that project.\n");
201
+ console.log("Note: Project .devto.json files are untouched run `devto unlink` per project to remove them.\n");
161
202
  }
162
203
  else {
163
204
  console.log("\nNo credentials found. Already logged out.\n");
164
205
  }
165
206
  }
166
207
  async function status() {
167
- const config = (0, config_1.readConfig)();
168
- if (!config) {
169
- console.log("\nNot logged in. Run `devto login` to authenticate.\n");
170
- process.exit(1);
171
- }
208
+ const projectJson = requireProjectJson();
209
+ const projectConfig = (0, config_1.readProjectConfig)();
210
+ const globalConfig = (0, config_1.readConfig)();
211
+ const resolved = projectConfig
212
+ ? { config: projectConfig, source: "project" }
213
+ : globalConfig
214
+ ? { config: globalConfig, source: "global" }
215
+ : null;
172
216
  console.log("\nDevTo Status");
173
217
  console.log("────────────");
218
+ console.log(`Project config: ${path.join(projectJson.dir, ".devto.json")}`);
219
+ console.log(` Workspace: ${projectJson.config.workspaceUrl}`);
220
+ console.log(` Project: ${projectJson.config.projectKey}`);
221
+ console.log(` Provider: ${projectJson.config.provider}`);
222
+ console.log();
223
+ if (!resolved) {
224
+ console.log("Auth: No API key found.");
225
+ console.log(" Run `devto init` to set up credentials for this project.\n");
226
+ process.exit(1);
227
+ }
228
+ const { config, source } = resolved;
229
+ if (source === "global") {
230
+ console.log("Auth source: global (~/.devto/config.json)");
231
+ }
232
+ else {
233
+ console.log("Auth source: project (.devto/config.json)");
234
+ }
174
235
  console.log(`API URL: ${config.api_url}`);
175
236
  console.log(`API Key: ${config.api_key.slice(0, 12)}...`);
176
237
  // Anthropic key status
177
- try {
238
+ const hasAnthropic = config.anthropic_key || (() => { try {
178
239
  (0, config_1.getAnthropicKey)();
179
- console.log("Anthropic Key: configured");
240
+ return true;
180
241
  }
181
242
  catch {
182
- console.log("Anthropic Key: missing (run `devto config set anthropic-key sk-ant-xxxx`)");
183
- }
243
+ return false;
244
+ } })();
245
+ console.log(`Anthropic Key: ${hasAnthropic ? "configured" : "missing (run `devto config set anthropic-key sk-ant-xxxx`)"}`);
184
246
  process.stdout.write("Connection: ");
185
247
  try {
186
- const res = await fetch(`${config.api_url}/api/v1/status`, {
187
- headers: {
188
- Authorization: `Bearer ${config.api_key}`,
189
- "X-DevTo-Version": "0.1.9",
190
- },
191
- });
248
+ const headers = {
249
+ Authorization: `Bearer ${config.api_key}`,
250
+ "X-DevTo-Version": config_1.VERSION,
251
+ };
252
+ if (projectJson.config.workspaceId) {
253
+ headers["X-DevTo-Workspace"] = projectJson.config.workspaceId;
254
+ }
255
+ const res = await fetch(`${config.api_url}/api/v1/status`, { headers });
192
256
  if (res.ok) {
193
257
  const data = (await res.json());
194
258
  console.log("Connected");
@@ -201,7 +265,7 @@ async function status() {
201
265
  }
202
266
  else if (res.status === 401) {
203
267
  console.log("Invalid API key");
204
- console.log("\nRun `devto login` to re-authenticate.");
268
+ console.log("\nRun `devto init` to re-authenticate.");
205
269
  }
206
270
  else {
207
271
  console.log(`Error (${res.status})`);
@@ -213,64 +277,172 @@ async function status() {
213
277
  console.log();
214
278
  }
215
279
  async function init() {
216
- console.log("\nDevTo Init — Configure Claude Code MCP\n");
217
- const config = (0, config_1.readConfig)();
218
- const apiKey = config?.api_key;
219
- if (!apiKey) {
220
- console.error("No API key found. Run `devto login` first, then re-run `devto init`.\n");
221
- process.exit(1);
280
+ console.log("\nDevTo Init — Link this project to DevTo\n");
281
+ const globalConfig = (0, config_1.readConfig)();
282
+ const existingProject = (0, config_1.readProjectConfig)();
283
+ const existingJson = (0, config_1.findProjectJson)(process.cwd());
284
+ const apiUrl = process.env.DEVTO_API_URL ?? globalConfig?.api_url ?? "https://api.devto.ai";
285
+ // ── Step 1: Get API key ──────────────────────────────────────────────
286
+ let apiKey;
287
+ if (existingProject?.api_key) {
288
+ const reuse = await prompt(`This project already has an API key (${existingProject.api_key.slice(0, 12)}...). Use it? (y/n): `);
289
+ if (reuse.toLowerCase() === "y" || reuse.toLowerCase() === "yes") {
290
+ apiKey = existingProject.api_key;
291
+ }
292
+ else {
293
+ apiKey = await promptForApiKey();
294
+ }
222
295
  }
223
- // Check for Anthropic key
224
- let anthropicKey;
225
- try {
226
- anthropicKey = (0, config_1.getAnthropicKey)();
296
+ else if (globalConfig?.api_key) {
297
+ const reuse = await prompt(`Use your global API key (${globalConfig.api_key.slice(0, 12)}...)? (y/n): `);
298
+ if (reuse.toLowerCase() === "y" || reuse.toLowerCase() === "yes") {
299
+ apiKey = globalConfig.api_key;
300
+ }
301
+ else {
302
+ apiKey = await promptForApiKey();
303
+ }
227
304
  }
228
- catch {
229
- // No Anthropic key yet — skip
305
+ else {
306
+ console.log("Get your API key from: https://devto.ai/dashboard/keys\n");
307
+ apiKey = await promptForApiKey();
230
308
  }
231
- // Remove existing devto server first (ignore errors if not present)
232
- console.log("Registering DevTo MCP server with Claude Code...\n");
233
- const { execSync } = require("child_process");
234
- try {
235
- execSync("claude mcp remove devto", { stdio: "ignore" });
309
+ // Validate the key
310
+ process.stdout.write("Validating... ");
311
+ const valid = await validateKey(apiKey);
312
+ if (!valid) {
313
+ console.error("\nInvalid API key. Check your key at https://devto.ai/dashboard/keys\n");
314
+ process.exit(1);
236
315
  }
237
- catch {
238
- // Not present fine
316
+ console.log("OK");
317
+ // ── Step 2: Select workspace/project ─────────────────────────────────
318
+ let workspaceUrl;
319
+ let projectKey;
320
+ let provider;
321
+ let workspaceId;
322
+ // Try to fetch workspaces from the API
323
+ console.log("\nFetching your workspaces...");
324
+ const workspaces = await fetchWorkspaces(apiKey);
325
+ if (workspaces.length > 0) {
326
+ console.log(`\nFound ${workspaces.length} workspace(s):\n`);
327
+ workspaces.forEach((ws, i) => {
328
+ console.log(` ${i + 1}. ${ws.name || ws.workspaceUrl} — ${ws.projectKey} (${ws.providerType})`);
329
+ });
330
+ let selected;
331
+ if (workspaces.length === 1) {
332
+ const confirm = await prompt(`\nUse this workspace? (y/n): `);
333
+ if (confirm.toLowerCase() !== "y" && confirm.toLowerCase() !== "yes") {
334
+ console.log("\nYou can connect a different workspace at https://devto.ai/dashboard/settings\n");
335
+ process.exit(0);
336
+ }
337
+ selected = workspaces[0];
338
+ }
339
+ else {
340
+ const choice = await prompt(`\nSelect workspace (1-${workspaces.length}): `);
341
+ const idx = parseInt(choice, 10) - 1;
342
+ if (isNaN(idx) || idx < 0 || idx >= workspaces.length) {
343
+ console.error("\nInvalid selection.\n");
344
+ process.exit(1);
345
+ }
346
+ selected = workspaces[idx];
347
+ }
348
+ workspaceUrl = selected.workspaceUrl;
349
+ projectKey = selected.projectKey;
350
+ provider = selected.providerType;
351
+ workspaceId = selected.id;
352
+ }
353
+ else {
354
+ // No workspaces found — prompt manually
355
+ console.log("\nNo workspaces found. Enter workspace details manually.\n");
356
+ workspaceUrl = await prompt("Workspace URL (e.g. https://yourcompany.atlassian.net): ");
357
+ if (!workspaceUrl) {
358
+ console.error("\nWorkspace URL is required.\n");
359
+ process.exit(1);
360
+ }
361
+ projectKey = await prompt("Project key (e.g. ENG): ");
362
+ if (!projectKey) {
363
+ console.error("\nProject key is required.\n");
364
+ process.exit(1);
365
+ }
366
+ provider = await prompt("Provider (jira/linear/github/shortcut) [jira]: ") || "jira";
239
367
  }
240
- // Use `claude mcp add-json` to register properly — handles quoting reliably
368
+ // ── Step 3: Check for Anthropic key ──────────────────────────────────
369
+ let anthropicKey = existingProject?.anthropic_key;
370
+ if (!anthropicKey) {
371
+ try {
372
+ anthropicKey = (0, config_1.getAnthropicKey)();
373
+ }
374
+ catch {
375
+ // No Anthropic key yet — skip
376
+ }
377
+ }
378
+ // ── Step 4: Write .devto.json (project config) ──────────────────────
379
+ (0, config_1.writeProjectJson)({
380
+ workspaceUrl,
381
+ projectKey,
382
+ provider,
383
+ workspaceId,
384
+ });
385
+ console.log("\nCreated .devto.json");
386
+ // Ensure .devto.json is gitignored
387
+ ensureGitignore(".devto.json");
388
+ // ── Step 5: Write .devto/config.json (credentials) ──────────────────
389
+ (0, config_1.writeProjectConfig)({
390
+ api_key: apiKey,
391
+ api_url: apiUrl,
392
+ anthropic_key: anthropicKey,
393
+ workspace_id: workspaceId,
394
+ });
395
+ // Ensure .devto/ is gitignored
396
+ ensureGitignore(".devto/");
397
+ console.log("Created .devto/config.json");
398
+ // ── Step 6: Register in ~/.devto/projects.json ──────────────────────
399
+ (0, config_1.registerProject)({
400
+ path: process.cwd(),
401
+ workspaceUrl,
402
+ projectKey,
403
+ provider,
404
+ });
405
+ console.log("Registered in ~/.devto/projects.json");
406
+ // ── Step 7: Write .mcp.json (Claude Code config) ────────────────────
241
407
  const isWindows = process.platform === "win32";
242
408
  const envObj = { DEVTO_API_KEY: apiKey };
243
409
  if (anthropicKey)
244
410
  envObj.DEVTO_ANTHROPIC_KEY = anthropicKey;
245
- const jsonConfig = JSON.stringify({
411
+ if (workspaceId)
412
+ envObj.DEVTO_WORKSPACE_ID = workspaceId;
413
+ const mcpServerConfig = {
246
414
  command: isWindows ? "cmd" : "npx",
247
415
  args: isWindows ? ["/c", "npx", "-y", "devto-mcp"] : ["-y", "devto-mcp"],
248
416
  env: envObj,
249
- });
417
+ };
418
+ const mcpJsonPath = path.join(process.cwd(), ".mcp.json");
419
+ let mcpConfig = {};
420
+ if (fs.existsSync(mcpJsonPath)) {
421
+ try {
422
+ mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
423
+ }
424
+ catch {
425
+ // Corrupt file — start fresh
426
+ }
427
+ }
428
+ if (!mcpConfig.mcpServers)
429
+ mcpConfig.mcpServers = {};
430
+ mcpConfig.mcpServers.devto = mcpServerConfig;
431
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
432
+ console.log("MCP config written to .mcp.json");
433
+ // Also remove any user-level devto MCP config to avoid conflicts
250
434
  try {
251
435
  const { execSync } = require("child_process");
252
- // Use single quotes in bash, escaped double quotes in cmd
253
- const escaped = isWindows
254
- ? jsonConfig.replace(/"/g, '\\"')
255
- : jsonConfig;
256
- const quote = isWindows ? '"' : "'";
257
- execSync(`claude mcp add-json devto ${quote}${escaped}${quote}`, { stdio: "inherit" });
258
- console.log("\nDevTo MCP server registered successfully.");
259
- console.log(`API key (${apiKey.slice(0, 12)}...) configured.`);
436
+ execSync("claude mcp remove devto", { stdio: "ignore" });
260
437
  }
261
438
  catch {
262
- console.error("\nFailed to register via Claude CLI. Is Claude Code installed?");
263
- console.error("Install: https://docs.anthropic.com/en/docs/claude-code\n");
264
- console.error("As a fallback, you can add manually via:");
265
- if (isWindows) {
266
- console.error(` claude mcp add -e DEVTO_API_KEY=${apiKey} devto -- cmd /c npx -y devto-mcp`);
267
- }
268
- else {
269
- console.error(` claude mcp add -e DEVTO_API_KEY=${apiKey} devto -- npx -y devto-mcp`);
270
- }
271
- process.exit(1);
439
+ // Not registered at user level fine
272
440
  }
273
- // Warn if Anthropic key is missing
441
+ console.log(`\n── Project linked ──────────────────────────────`);
442
+ console.log(`Workspace: ${workspaceUrl}`);
443
+ console.log(`Project: ${projectKey}`);
444
+ console.log(`Provider: ${provider}`);
445
+ console.log(`API key: ${apiKey.slice(0, 12)}...`);
274
446
  if (!anthropicKey) {
275
447
  console.log("\nNote: Anthropic API key not configured.");
276
448
  console.log(" AI plan generation (create_plan) requires an Anthropic key.");
@@ -280,9 +452,90 @@ async function init() {
280
452
  }
281
453
  console.log("\nRestart Claude Code to pick up the changes.\n");
282
454
  }
455
+ async function unlink() {
456
+ console.log("\nDevTo Unlink — Remove this project from DevTo\n");
457
+ const projectJson = (0, config_1.findProjectJson)(process.cwd());
458
+ const devtoJsonPath = path.join(process.cwd(), ".devto.json");
459
+ const devtoConfigDir = path.join(process.cwd(), ".devto");
460
+ if (!projectJson && !fs.existsSync(devtoJsonPath) && !fs.existsSync(devtoConfigDir)) {
461
+ console.log("This project is not linked to DevTo.\n");
462
+ return;
463
+ }
464
+ const confirm = await prompt("Remove DevTo configuration from this project? (y/n): ");
465
+ if (confirm.toLowerCase() !== "y" && confirm.toLowerCase() !== "yes") {
466
+ console.log("Cancelled.\n");
467
+ return;
468
+ }
469
+ // Remove .devto.json
470
+ if (fs.existsSync(devtoJsonPath)) {
471
+ fs.unlinkSync(devtoJsonPath);
472
+ console.log("Removed .devto.json");
473
+ }
474
+ // Remove .devto/ directory
475
+ if (fs.existsSync(devtoConfigDir)) {
476
+ fs.rmSync(devtoConfigDir, { recursive: true, force: true });
477
+ console.log("Removed .devto/");
478
+ }
479
+ // Remove devto entry from .mcp.json
480
+ const mcpJsonPath = path.join(process.cwd(), ".mcp.json");
481
+ if (fs.existsSync(mcpJsonPath)) {
482
+ try {
483
+ const raw = fs.readFileSync(mcpJsonPath, "utf-8");
484
+ const mcpConfig = JSON.parse(raw);
485
+ if (mcpConfig.mcpServers?.devto) {
486
+ delete mcpConfig.mcpServers.devto;
487
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
488
+ console.log("Removed devto from .mcp.json");
489
+ }
490
+ }
491
+ catch {
492
+ // Skip corrupt config
493
+ }
494
+ }
495
+ // Unregister from ~/.devto/projects.json
496
+ const removed = (0, config_1.unregisterProject)(process.cwd());
497
+ if (removed) {
498
+ console.log("Removed from ~/.devto/projects.json");
499
+ }
500
+ console.log("\nProject unlinked. Global credentials (~/.devto/config.json) are untouched.");
501
+ console.log("Restart Claude Code to apply changes.\n");
502
+ }
503
+ async function promptForApiKey() {
504
+ const apiKey = await prompt("Paste your API key: ", true);
505
+ if (!apiKey || !apiKey.startsWith("devto_")) {
506
+ console.error("\nInvalid key format. DevTo API keys start with 'devto_'.\nGet your key at https://devto.ai/dashboard/keys\n");
507
+ process.exit(1);
508
+ }
509
+ return apiKey;
510
+ }
511
+ function ensureGitignore(entry) {
512
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
513
+ try {
514
+ if (fs.existsSync(gitignorePath)) {
515
+ const content = fs.readFileSync(gitignorePath, "utf-8");
516
+ if (!content.includes(entry)) {
517
+ const label = entry === ".devto.json" ? "DevTo project config" : "DevTo credentials (contains API keys)";
518
+ fs.appendFileSync(gitignorePath, `\n# ${label}\n${entry}\n`);
519
+ }
520
+ }
521
+ else {
522
+ const label = entry === ".devto.json" ? "DevTo project config" : "DevTo credentials (contains API keys)";
523
+ fs.writeFileSync(gitignorePath, `# ${label}\n${entry}\n`);
524
+ }
525
+ }
526
+ catch {
527
+ console.log(` Warning: Could not update .gitignore — make sure ${entry} is not committed.`);
528
+ }
529
+ }
283
530
  async function doctor() {
284
531
  console.log("\nDevTo Doctor — Connection Diagnostics\n");
285
- const config = (0, config_1.readConfig)();
532
+ const projectJson = requireProjectJson();
533
+ const resolved = (0, config_1.resolveConfig)();
534
+ const config = resolved?.config ?? null;
535
+ process.stdout.write("Project config (.devto.json) ... ");
536
+ console.log(`OK (${path.join(projectJson.dir, ".devto.json")})`);
537
+ console.log(` Workspace: ${projectJson.config.workspaceUrl}`);
538
+ console.log(` Project: ${projectJson.config.projectKey} (${projectJson.config.provider})`);
286
539
  // 1. Check DevTo API reachable
287
540
  process.stdout.write("DevTo API reachable ... ");
288
541
  const apiUrl = config?.api_url ?? (0, config_1.getApiUrl)();
@@ -290,7 +543,7 @@ async function doctor() {
290
543
  const res = await fetch(`${apiUrl}/api/v1/status`, {
291
544
  headers: {
292
545
  ...(config?.api_key ? { Authorization: `Bearer ${config.api_key}` } : {}),
293
- "X-DevTo-Version": "0.1.9",
546
+ "X-DevTo-Version": config_1.VERSION,
294
547
  },
295
548
  });
296
549
  if (res.ok) {
@@ -316,7 +569,7 @@ async function doctor() {
316
569
  const res = await fetch(`${apiUrl}/api/v1/status`, {
317
570
  headers: {
318
571
  Authorization: `Bearer ${config.api_key}`,
319
- "X-DevTo-Version": "0.1.9",
572
+ "X-DevTo-Version": config_1.VERSION,
320
573
  },
321
574
  });
322
575
  if (res.status === 401) {
@@ -344,7 +597,7 @@ async function doctor() {
344
597
  const res = await fetch(`${apiUrl}/api/v1/status`, {
345
598
  headers: {
346
599
  Authorization: `Bearer ${config.api_key}`,
347
- "X-DevTo-Version": "0.1.9",
600
+ "X-DevTo-Version": config_1.VERSION,
348
601
  },
349
602
  });
350
603
  if (res.ok) {
@@ -402,6 +655,7 @@ async function doctor() {
402
655
  }
403
656
  async function sync() {
404
657
  console.log("\nDevTo Sync — Workspace Configuration Discovery\n");
658
+ requireProjectJson();
405
659
  const config = (0, config_1.readConfig)();
406
660
  if (!config?.api_key) {
407
661
  console.log("Not logged in. Run `devto login` first.\n");
@@ -415,7 +669,7 @@ async function sync() {
415
669
  headers: {
416
670
  "Content-Type": "application/json",
417
671
  Authorization: `Bearer ${config.api_key}`,
418
- "X-DevTo-Version": "0.1.9",
672
+ "X-DevTo-Version": config_1.VERSION,
419
673
  },
420
674
  });
421
675
  if (!res.ok) {
@@ -464,64 +718,6 @@ async function configSet() {
464
718
  console.log(`Key: ${value.slice(0, 12)}...`);
465
719
  console.log("Run `devto init` to embed it in your project's MCP config.\n");
466
720
  }
467
- async function uninstall() {
468
- const purge = process.argv[3] === "--purge";
469
- if (purge) {
470
- console.log("\nDevTo Uninstall (full purge)\n");
471
- const confirm = await prompt("This will remove DevTo MCP server AND delete global credentials (~/.devto). Continue? (y/n): ");
472
- if (confirm.toLowerCase() !== "y" && confirm.toLowerCase() !== "yes") {
473
- console.log("Cancelled.\n");
474
- return;
475
- }
476
- }
477
- else {
478
- console.log("\nDevTo Uninstall\n");
479
- const confirm = await prompt("Remove DevTo MCP server from Claude Code? (y/n): ");
480
- if (confirm.toLowerCase() !== "y" && confirm.toLowerCase() !== "yes") {
481
- console.log("Cancelled.\n");
482
- return;
483
- }
484
- }
485
- // Remove via Claude CLI
486
- const { spawnSync } = require("child_process");
487
- try {
488
- spawnSync("claude", ["mcp", "remove", "devto"], { stdio: "inherit", shell: true });
489
- console.log("Removed DevTo MCP server from Claude Code.");
490
- }
491
- catch {
492
- console.log("Could not remove via Claude CLI (may not be registered).");
493
- }
494
- // Also clean up .mcp.json if it has a devto entry (legacy)
495
- const mcpJsonPath = path.join(process.cwd(), ".mcp.json");
496
- if (fs.existsSync(mcpJsonPath)) {
497
- try {
498
- const raw = fs.readFileSync(mcpJsonPath, "utf-8");
499
- const mcpConfig = JSON.parse(raw);
500
- if (mcpConfig.mcpServers?.devto) {
501
- delete mcpConfig.mcpServers.devto;
502
- fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
503
- console.log(`Also removed devto from: ${mcpJsonPath}`);
504
- }
505
- }
506
- catch {
507
- // Skip corrupt config
508
- }
509
- }
510
- // Only with --purge: remove global ~/.devto config
511
- if (purge) {
512
- const configDir = path.join(os.homedir(), ".devto");
513
- if (fs.existsSync(configDir)) {
514
- fs.rmSync(configDir, { recursive: true, force: true });
515
- console.log("Removed ~/.devto global credentials.");
516
- }
517
- console.log("\nFull purge complete. To finish, run: npm uninstall -g devto-mcp\n");
518
- }
519
- else {
520
- console.log("\nDevTo removed from this project.");
521
- console.log("Your global credentials (~/.devto) are untouched — other projects still work.");
522
- console.log("Restart Claude Code to apply changes.\n");
523
- }
524
- }
525
721
  function printHelp() {
526
722
  console.log(`
527
723
  DevTo CLI — AI-powered work management
@@ -529,26 +725,26 @@ DevTo CLI — AI-powered work management
529
725
  Usage:
530
726
  devto login Authenticate with your DevTo API key
531
727
  devto logout Clear global credentials (~/.devto)
532
- devto status Show connection status
533
- devto init Auto-configure Claude Code MCP config
728
+ devto status Show connection status and active project
729
+ devto init Link this project to DevTo
730
+ devto unlink Unlink this project from DevTo
534
731
  devto doctor Test full connection chain
535
732
  devto sync Re-sync workspace configuration
536
733
  devto verbose Toggle verbose mode
537
734
  devto config set anthropic-key <key> Store Anthropic API key
538
- devto uninstall Remove DevTo from this project
539
- devto uninstall --purge Remove from project + delete global credentials
540
735
  devto help Show this help message
541
736
  devto --version Show installed version
542
737
 
543
- After logging in, run \`devto init\` in your project to add DevTo to .mcp.json.
738
+ Setup:
739
+ 1. npm install -g devto-mcp Install globally (once)
740
+ 2. devto login Save your API key
741
+ 3. devto init Link project, create .devto.json + .mcp.json
742
+ 4. Restart Claude Code
544
743
 
545
- Manual setup (add to your project's .mcp.json):
546
-
547
- Windows:
548
- { "mcpServers": { "devto": { "command": "cmd", "args": ["/c", "npx", "-y", "devto-mcp"], "env": { "DEVTO_API_KEY": "your-key" } } } }
549
-
550
- macOS/Linux:
551
- { "mcpServers": { "devto": { "command": "npx", "args": ["-y", "devto-mcp"], "env": { "DEVTO_API_KEY": "your-key" } } } }
744
+ Per-project config (.devto.json):
745
+ Created by \`devto init\`. Contains workspace URL, project key, and provider.
746
+ Add .devto.json to .gitignore (devto init does this automatically).
747
+ To disconnect a project: \`devto unlink\`
552
748
  `);
553
749
  }
554
750
  async function main() {
@@ -569,6 +765,10 @@ async function main() {
569
765
  case "--init":
570
766
  await init();
571
767
  break;
768
+ case "unlink":
769
+ case "--unlink":
770
+ await unlink();
771
+ break;
572
772
  case "doctor":
573
773
  case "--doctor":
574
774
  await doctor();
@@ -585,13 +785,9 @@ async function main() {
585
785
  case "--config":
586
786
  await configSet();
587
787
  break;
588
- case "uninstall":
589
- case "--uninstall":
590
- await uninstall();
591
- break;
592
788
  case "--version":
593
789
  case "-v":
594
- console.log(`devto-mcp v${require("../package.json").version}`);
790
+ console.log(`devto-mcp v${config_1.VERSION}`);
595
791
  break;
596
792
  case "help":
597
793
  case "--help":