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/README.md +48 -4
- package/bin/devto-mcp.js +0 -0
- package/bin/devto.js +0 -0
- package/dist/cli.d.ts +2 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +341 -145
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -6
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +55 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +169 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +18 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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":
|
|
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`
|
|
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 .
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
240
|
+
return true;
|
|
180
241
|
}
|
|
181
242
|
catch {
|
|
182
|
-
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
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 —
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
229
|
-
|
|
305
|
+
else {
|
|
306
|
+
console.log("Get your API key from: https://devto.ai/dashboard/keys\n");
|
|
307
|
+
apiKey = await promptForApiKey();
|
|
230
308
|
}
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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${
|
|
790
|
+
console.log(`devto-mcp v${config_1.VERSION}`);
|
|
595
791
|
break;
|
|
596
792
|
case "help":
|
|
597
793
|
case "--help":
|