devto-mcp 0.5.3 → 0.7.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.
- package/README.md +7 -4
- package/dist/cli.d.ts +4 -4
- package/dist/cli.js +199 -88
- package/dist/cli.js.map +1 -1
- package/dist/client.js +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +20 -12
- package/dist/config.js.map +1 -1
- package/dist/index.js +71 -26
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +118 -59
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,12 +14,11 @@ This is a one-time global install. Each project is linked individually via `devt
|
|
|
14
14
|
|
|
15
15
|
## Setup
|
|
16
16
|
|
|
17
|
-
1. Create an account
|
|
17
|
+
1. Create an account at [devto.ai](https://devto.ai). Connect Jira from the onboarding screen at [devto.ai/onboard](https://devto.ai/onboard) — click **Connect with Jira** and authorize via OAuth. No tokens to copy.
|
|
18
18
|
|
|
19
|
-
2.
|
|
19
|
+
2. Configure your Anthropic key:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
devto login
|
|
23
22
|
devto config set anthropic-key sk-ant-xxxx
|
|
24
23
|
```
|
|
25
24
|
|
|
@@ -30,6 +29,10 @@ cd your-project
|
|
|
30
29
|
devto init
|
|
31
30
|
```
|
|
32
31
|
|
|
32
|
+
`devto init` opens your browser to `devto.ai/cli/authorize`. A short code is displayed — confirm it matches in the browser, click **Approve**, and the CLI receives your API key silently. No copy-pasting.
|
|
33
|
+
|
|
34
|
+
If you prefer to paste an API key manually, select option **[2]** at the prompt. Get your key from [devto.ai/dashboard/keys](https://devto.ai/dashboard/keys).
|
|
35
|
+
|
|
33
36
|
This creates:
|
|
34
37
|
- `.devto.json` — project config (workspace URL, project key, provider)
|
|
35
38
|
- `.devto/config.json` — credentials (API key, scoped to this project)
|
|
@@ -69,7 +72,7 @@ DevTo generates a structured plan. Run `confirm_plan` to push it to your project
|
|
|
69
72
|
|
|
70
73
|
| Command | Description |
|
|
71
74
|
|---|---|
|
|
72
|
-
| `devto login` | Authenticate
|
|
75
|
+
| `devto login` | Authenticate globally (browser flow or paste API key) |
|
|
73
76
|
| `devto init` | Link this project to DevTo |
|
|
74
77
|
| `devto unlink` | Unlink this project (removes .devto.json, .devto/, registry entry) |
|
|
75
78
|
| `devto status` | Show connection and active project config |
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* devto CLI — devto <command>
|
|
4
4
|
*
|
|
5
5
|
* Commands:
|
|
6
|
-
* devto login Authenticate with your
|
|
6
|
+
* devto login Authenticate with your devto API key
|
|
7
7
|
* devto logout Clear global credentials
|
|
8
8
|
* devto status Show current connection status
|
|
9
|
-
* devto init Link this project to
|
|
10
|
-
* devto unlink Unlink this project from
|
|
9
|
+
* devto init Link this project to devto
|
|
10
|
+
* devto unlink Unlink this project from devto
|
|
11
11
|
* devto doctor Test full connection chain
|
|
12
12
|
* devto sync Re-sync workspace configuration
|
|
13
13
|
* devto verbose Toggle verbose mode
|
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* devto CLI — devto <command>
|
|
5
5
|
*
|
|
6
6
|
* Commands:
|
|
7
|
-
* devto login Authenticate with your
|
|
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 Link this project to
|
|
11
|
-
* devto unlink Unlink this project from
|
|
10
|
+
* devto init Link this project to devto
|
|
11
|
+
* devto unlink Unlink this project from devto
|
|
12
12
|
* devto doctor Test full connection chain
|
|
13
13
|
* devto sync Re-sync workspace configuration
|
|
14
14
|
* devto verbose Toggle verbose mode
|
|
@@ -54,7 +54,7 @@ const path = __importStar(require("path"));
|
|
|
54
54
|
const os = __importStar(require("os"));
|
|
55
55
|
const config_1 = require("./config");
|
|
56
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
|
|
57
|
+
const NO_PROJECT_MESSAGE = `\nNo project linked in this directory.\nRun \`devto init\` to link this project to a devto workspace.\n`;
|
|
58
58
|
/**
|
|
59
59
|
* Require .devto.json in the directory tree.
|
|
60
60
|
* If not found, print message and exit. Used by status, doctor, sync.
|
|
@@ -180,14 +180,116 @@ async function fetchWorkspaces(apiKey) {
|
|
|
180
180
|
return [];
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Open a URL in the user's default browser (best-effort, non-blocking).
|
|
185
|
+
* Silently swallows failures — the CLI also prints the URL so manual copy-paste works.
|
|
186
|
+
*/
|
|
187
|
+
function openBrowser(url) {
|
|
188
|
+
try {
|
|
189
|
+
const { spawn } = require("child_process");
|
|
190
|
+
const platform = process.platform;
|
|
191
|
+
if (platform === "darwin") {
|
|
192
|
+
spawn("open", [url], { stdio: "ignore", detached: true }).unref();
|
|
193
|
+
}
|
|
194
|
+
else if (platform === "win32") {
|
|
195
|
+
spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true, shell: false }).unref();
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
/* ignore — user can click the printed URL */
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Device-code authorization: open the browser, display a short code, and poll
|
|
207
|
+
* the API until the user approves. Returns the issued API key. Throws on
|
|
208
|
+
* timeout, denial, or network failure.
|
|
209
|
+
*/
|
|
210
|
+
async function browserLogin() {
|
|
211
|
+
const apiUrl = process.env.DEVTO_API_URL ?? "https://api.devto.ai";
|
|
212
|
+
let start;
|
|
213
|
+
try {
|
|
214
|
+
const res = await fetch(`${apiUrl}/api/v1/cli/device/start`, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "Content-Type": "application/json", "X-DevTo-Version": config_1.VERSION },
|
|
217
|
+
body: "{}",
|
|
218
|
+
});
|
|
219
|
+
if (!res.ok)
|
|
220
|
+
throw new Error(`${res.status}`);
|
|
221
|
+
start = (await res.json());
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
throw new Error("Could not reach devto to start browser sign-in. Check your internet connection.");
|
|
225
|
+
}
|
|
226
|
+
const openUrl = start.verification_url_complete ?? start.verification_url ?? "https://devto.ai/cli/authorize";
|
|
227
|
+
console.log("\nOpening browser to sign in...");
|
|
228
|
+
console.log(` ${openUrl}`);
|
|
229
|
+
console.log(`\n Verify the code matches: ${start.user_code}\n`);
|
|
230
|
+
openBrowser(openUrl);
|
|
231
|
+
// ── Poll until approved / denied / expired ──────────────────────────
|
|
232
|
+
const deadline = Date.now() + start.expires_in * 1000;
|
|
233
|
+
const intervalMs = Math.max(2, start.interval) * 1000;
|
|
234
|
+
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
235
|
+
let frame = 0;
|
|
236
|
+
const spinner = setInterval(() => {
|
|
237
|
+
process.stdout.write(`\r ${spinnerFrames[frame++ % spinnerFrames.length]} Waiting for you to authorize...`);
|
|
238
|
+
}, 100);
|
|
239
|
+
try {
|
|
240
|
+
while (Date.now() < deadline) {
|
|
241
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
242
|
+
let res;
|
|
243
|
+
try {
|
|
244
|
+
res = await fetch(`${apiUrl}/api/v1/cli/device/poll`, {
|
|
245
|
+
method: "POST",
|
|
246
|
+
headers: { "Content-Type": "application/json", "X-DevTo-Version": config_1.VERSION },
|
|
247
|
+
body: JSON.stringify({ device_code: start.device_code }),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
continue; // transient network failure — keep polling
|
|
252
|
+
}
|
|
253
|
+
if (res.status === 200) {
|
|
254
|
+
const data = (await res.json());
|
|
255
|
+
if (data.status === "approved" && data.api_key) {
|
|
256
|
+
clearInterval(spinner);
|
|
257
|
+
process.stdout.write("\r\x1b[K"); // clear spinner line
|
|
258
|
+
console.log(" Authorized.\n");
|
|
259
|
+
return data.api_key;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else if (res.status === 202) {
|
|
263
|
+
continue; // still pending
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
const data = (await res.json().catch(() => ({})));
|
|
267
|
+
clearInterval(spinner);
|
|
268
|
+
process.stdout.write("\r\x1b[K");
|
|
269
|
+
if (data.code === "CLI_DEVICE_DENIED") {
|
|
270
|
+
throw new Error("Authorization was denied.");
|
|
271
|
+
}
|
|
272
|
+
if (data.code === "CLI_DEVICE_EXPIRED") {
|
|
273
|
+
throw new Error("Authorization expired. Run `devto init` again.");
|
|
274
|
+
}
|
|
275
|
+
throw new Error(data.message ?? "Authorization failed.");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
throw new Error("Timed out waiting for authorization. Run `devto init` again.");
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
clearInterval(spinner);
|
|
282
|
+
process.stdout.write("\r\x1b[K");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
183
285
|
async function login() {
|
|
184
|
-
console.log("\
|
|
286
|
+
console.log("\ndevto Login (global)\n");
|
|
185
287
|
console.log("Note: For per-project isolation, use `devto init` instead.");
|
|
186
288
|
console.log(" `devto login` sets a global fallback key.\n");
|
|
187
289
|
console.log("Get your API key from: https://devto.ai/dashboard/keys\n");
|
|
188
290
|
const apiKey = await prompt("Paste your API key: ", true);
|
|
189
291
|
if (!apiKey || !apiKey.startsWith("devto_")) {
|
|
190
|
-
console.error("\nInvalid key format.
|
|
292
|
+
console.error("\nInvalid key format. devto API keys start with 'devto_'.\nGet your key at https://devto.ai/dashboard/keys\n");
|
|
191
293
|
process.exit(1);
|
|
192
294
|
}
|
|
193
295
|
process.stdout.write("Validating... ");
|
|
@@ -197,42 +299,28 @@ async function login() {
|
|
|
197
299
|
process.exit(1);
|
|
198
300
|
}
|
|
199
301
|
if (result === "unreachable") {
|
|
200
|
-
console.error("\nCould not reach
|
|
302
|
+
console.error("\nCould not reach devto API. Check your internet connection.\n");
|
|
201
303
|
process.exit(1);
|
|
202
304
|
}
|
|
203
305
|
const apiUrl = process.env.DEVTO_API_URL ?? "https://api.devto.ai";
|
|
204
306
|
(0, config_1.writeConfig)({ api_key: apiKey, api_url: apiUrl });
|
|
205
307
|
console.log("OK\n");
|
|
206
308
|
console.log("Logged in! Your API key is saved to ~/.devto/config.json\n");
|
|
207
|
-
// Prompt for Anthropic key
|
|
208
|
-
console.log("────────────────────────────────────────────────");
|
|
209
|
-
console.log("DevTo uses the Anthropic API to generate AI plans locally.");
|
|
210
|
-
console.log("You need an Anthropic API key for the `create_plan` feature.");
|
|
211
|
-
console.log("Get one at: https://console.anthropic.com/settings/keys\n");
|
|
212
|
-
const setupAnthropic = await prompt("Set up Anthropic key now? (y/n): ");
|
|
213
|
-
if (setupAnthropic.toLowerCase() === "y" || setupAnthropic.toLowerCase() === "yes") {
|
|
214
|
-
const anthropicKey = await prompt("Paste your Anthropic API key: ", true);
|
|
215
|
-
if (!anthropicKey || !anthropicKey.startsWith("sk-ant-")) {
|
|
216
|
-
console.error("\nInvalid key format. Anthropic keys start with 'sk-ant-'.");
|
|
217
|
-
console.log("You can set it later: devto config set anthropic-key sk-ant-xxxx\n");
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
(0, config_1.setAnthropicKey)(anthropicKey);
|
|
221
|
-
console.log("Anthropic key saved!\n");
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
console.log("\nSkipped. You can set it later: devto config set anthropic-key sk-ant-xxxx\n");
|
|
226
|
-
}
|
|
227
309
|
console.log("Next step: run `devto init` in your project directory.");
|
|
228
310
|
console.log("Run `devto status` to verify your connection.\n");
|
|
229
311
|
}
|
|
230
312
|
async function logout() {
|
|
231
313
|
const configDir = path.join(os.homedir(), ".devto");
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
314
|
+
const configFile = path.join(configDir, "config.json");
|
|
315
|
+
let removed = false;
|
|
316
|
+
if (fs.existsSync(configFile)) {
|
|
317
|
+
fs.unlinkSync(configFile);
|
|
318
|
+
removed = true;
|
|
319
|
+
}
|
|
320
|
+
if (removed) {
|
|
321
|
+
console.log("\nLogged out. Removed ~/.devto/config.json credentials.");
|
|
322
|
+
console.log("Note: ~/.devto/projects.json and project .devto.json files are untouched.");
|
|
323
|
+
console.log("Run `devto unlink` per project to remove project-level config.\n");
|
|
236
324
|
}
|
|
237
325
|
else {
|
|
238
326
|
console.log("\nNo credentials found. Already logged out.\n");
|
|
@@ -247,7 +335,7 @@ async function status() {
|
|
|
247
335
|
: globalConfig
|
|
248
336
|
? { config: globalConfig, source: "global" }
|
|
249
337
|
: null;
|
|
250
|
-
console.log("\
|
|
338
|
+
console.log("\ndevto Status");
|
|
251
339
|
console.log("────────────");
|
|
252
340
|
console.log(`Project config: ${path.join(projectJson.dir, ".devto.json")}`);
|
|
253
341
|
console.log(` Workspace: ${projectJson.config.workspaceUrl}`);
|
|
@@ -268,15 +356,18 @@ async function status() {
|
|
|
268
356
|
}
|
|
269
357
|
console.log(`API URL: ${config.api_url}`);
|
|
270
358
|
console.log(`API Key: ${config.api_key.slice(0, 12)}...`);
|
|
271
|
-
//
|
|
272
|
-
const
|
|
273
|
-
(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
359
|
+
// AI call routing
|
|
360
|
+
const hasLocalAnthropicKey = config.anthropic_key ||
|
|
361
|
+
(() => {
|
|
362
|
+
try {
|
|
363
|
+
(0, config_1.getAnthropicKey)();
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
})();
|
|
370
|
+
console.log(`AI calls: ${hasLocalAnthropicKey ? "your own key (Expert Mode)" : "via devto"}`);
|
|
280
371
|
process.stdout.write("Connection: ");
|
|
281
372
|
try {
|
|
282
373
|
const headers = {
|
|
@@ -306,25 +397,35 @@ async function status() {
|
|
|
306
397
|
}
|
|
307
398
|
}
|
|
308
399
|
catch {
|
|
309
|
-
console.log("Could not connect to
|
|
400
|
+
console.log("Could not connect to devto API");
|
|
310
401
|
}
|
|
311
402
|
console.log();
|
|
312
403
|
}
|
|
313
404
|
async function init() {
|
|
314
|
-
console.log("\
|
|
405
|
+
console.log("\ndevto Init — Link this project to devto\n");
|
|
315
406
|
const globalConfig = (0, config_1.readConfig)();
|
|
316
407
|
const existingProject = (0, config_1.readProjectConfig)();
|
|
317
408
|
const existingJson = (0, config_1.findProjectJson)(process.cwd());
|
|
318
409
|
const apiUrl = process.env.DEVTO_API_URL ?? globalConfig?.api_url ?? "https://api.devto.ai";
|
|
319
410
|
// ── Step 1: Get API key ──────────────────────────────────────────────
|
|
411
|
+
// Preferred: browser-based device authorization (no key copy-paste).
|
|
412
|
+
// Fallbacks: reuse an existing project/global key, or paste one manually.
|
|
320
413
|
let apiKey;
|
|
414
|
+
async function acquireKeyInteractively() {
|
|
415
|
+
const method = await prompt("Sign in? [1] Browser (recommended) [2] Paste API key: ");
|
|
416
|
+
if (method.trim() === "2") {
|
|
417
|
+
console.log("Get your API key from: https://devto.ai/dashboard/keys\n");
|
|
418
|
+
return promptForApiKey();
|
|
419
|
+
}
|
|
420
|
+
return browserLogin();
|
|
421
|
+
}
|
|
321
422
|
if (existingProject?.api_key) {
|
|
322
423
|
const reuse = await prompt(`This project already has an API key (${existingProject.api_key.slice(0, 12)}...). Use it? (y/n): `);
|
|
323
424
|
if (reuse.toLowerCase() === "y" || reuse.toLowerCase() === "yes") {
|
|
324
425
|
apiKey = existingProject.api_key;
|
|
325
426
|
}
|
|
326
427
|
else {
|
|
327
|
-
apiKey = await
|
|
428
|
+
apiKey = await acquireKeyInteractively();
|
|
328
429
|
}
|
|
329
430
|
}
|
|
330
431
|
else if (globalConfig?.api_key) {
|
|
@@ -333,12 +434,11 @@ async function init() {
|
|
|
333
434
|
apiKey = globalConfig.api_key;
|
|
334
435
|
}
|
|
335
436
|
else {
|
|
336
|
-
apiKey = await
|
|
437
|
+
apiKey = await acquireKeyInteractively();
|
|
337
438
|
}
|
|
338
439
|
}
|
|
339
440
|
else {
|
|
340
|
-
|
|
341
|
-
apiKey = await promptForApiKey();
|
|
441
|
+
apiKey = await acquireKeyInteractively();
|
|
342
442
|
}
|
|
343
443
|
// Validate the key
|
|
344
444
|
process.stdout.write("Validating... ");
|
|
@@ -348,7 +448,7 @@ async function init() {
|
|
|
348
448
|
process.exit(1);
|
|
349
449
|
}
|
|
350
450
|
if (validation === "unreachable") {
|
|
351
|
-
console.error("\nCould not reach
|
|
451
|
+
console.error("\nCould not reach devto API. Check your internet connection.\n");
|
|
352
452
|
process.exit(1);
|
|
353
453
|
}
|
|
354
454
|
console.log("OK");
|
|
@@ -388,16 +488,15 @@ async function init() {
|
|
|
388
488
|
const projectKey = selected.projectKey;
|
|
389
489
|
const provider = selected.providerType;
|
|
390
490
|
const workspaceId = selected.id;
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
if (!anthropicKey) {
|
|
491
|
+
// Preserve any existing Anthropic key for Expert Mode users
|
|
492
|
+
const anthropicKey = existingProject?.anthropic_key ?? (() => {
|
|
394
493
|
try {
|
|
395
|
-
|
|
494
|
+
return (0, config_1.getAnthropicKey)();
|
|
396
495
|
}
|
|
397
496
|
catch {
|
|
398
|
-
|
|
497
|
+
return undefined;
|
|
399
498
|
}
|
|
400
|
-
}
|
|
499
|
+
})();
|
|
401
500
|
// ── Step 4: Write .devto.json (project config) ──────────────────────
|
|
402
501
|
(0, config_1.writeProjectJson)({
|
|
403
502
|
workspaceUrl,
|
|
@@ -456,6 +555,15 @@ async function init() {
|
|
|
456
555
|
}
|
|
457
556
|
if (!mcpConfig.mcpServers)
|
|
458
557
|
mcpConfig.mcpServers = {};
|
|
558
|
+
// Warn if overwriting a different user's credentials
|
|
559
|
+
const existingMcp = mcpConfig.mcpServers.devto;
|
|
560
|
+
if (existingMcp?.env?.DEVTO_API_KEY && existingMcp.env.DEVTO_API_KEY !== apiKey) {
|
|
561
|
+
const overwrite = await prompt(`\n⚠ .mcp.json already has a different devto API key (${existingMcp.env.DEVTO_API_KEY.slice(0, 12)}...).\nOverwrite with yours? (y/n): `);
|
|
562
|
+
if (overwrite.toLowerCase() !== "y" && overwrite.toLowerCase() !== "yes") {
|
|
563
|
+
console.log("\nAborted. Existing .mcp.json left unchanged.\n");
|
|
564
|
+
process.exit(0);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
459
567
|
mcpConfig.mcpServers.devto = mcpServerConfig;
|
|
460
568
|
fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
|
|
461
569
|
console.log("MCP config written to .mcp.json");
|
|
@@ -478,25 +586,21 @@ async function init() {
|
|
|
478
586
|
console.log(`Project: ${projectKey}`);
|
|
479
587
|
console.log(`Provider: ${provider}`);
|
|
480
588
|
console.log(`API key: ${apiKey.slice(0, 12)}...`);
|
|
481
|
-
if (
|
|
482
|
-
console.log("
|
|
483
|
-
console.log(" AI plan generation (create_plan) requires an Anthropic key.");
|
|
484
|
-
console.log(" Run: devto config set anthropic-key sk-ant-xxxx");
|
|
485
|
-
console.log(" Then re-run `devto init` to update the MCP config.");
|
|
486
|
-
console.log(" Get one at: https://console.anthropic.com/settings/keys");
|
|
589
|
+
if (anthropicKey) {
|
|
590
|
+
console.log("AI calls: Expert Mode (your own Anthropic key)");
|
|
487
591
|
}
|
|
488
592
|
console.log("\nRestart Claude Code to pick up the changes.\n");
|
|
489
593
|
}
|
|
490
594
|
async function unlink() {
|
|
491
|
-
console.log("\
|
|
595
|
+
console.log("\ndevto Unlink — Remove this project from devto\n");
|
|
492
596
|
const projectJson = (0, config_1.findProjectJson)(process.cwd());
|
|
493
597
|
const devtoJsonPath = path.join(process.cwd(), ".devto.json");
|
|
494
598
|
const devtoConfigDir = path.join(process.cwd(), ".devto");
|
|
495
599
|
if (!projectJson && !fs.existsSync(devtoJsonPath) && !fs.existsSync(devtoConfigDir)) {
|
|
496
|
-
console.log("This project is not linked to
|
|
600
|
+
console.log("This project is not linked to devto.\n");
|
|
497
601
|
return;
|
|
498
602
|
}
|
|
499
|
-
const confirm = await prompt("Remove
|
|
603
|
+
const confirm = await prompt("Remove devto configuration from this project? (y/n): ");
|
|
500
604
|
if (confirm.toLowerCase() !== "y" && confirm.toLowerCase() !== "yes") {
|
|
501
605
|
console.log("Cancelled.\n");
|
|
502
606
|
return;
|
|
@@ -546,7 +650,7 @@ async function unlink() {
|
|
|
546
650
|
async function promptForApiKey() {
|
|
547
651
|
const apiKey = await prompt("Paste your API key: ", true);
|
|
548
652
|
if (!apiKey || !apiKey.startsWith("devto_")) {
|
|
549
|
-
console.error("\nInvalid key format.
|
|
653
|
+
console.error("\nInvalid key format. devto API keys start with 'devto_'.\nGet your key at https://devto.ai/dashboard/keys\n");
|
|
550
654
|
process.exit(1);
|
|
551
655
|
}
|
|
552
656
|
return apiKey;
|
|
@@ -556,17 +660,17 @@ function ensureGitignore(entry) {
|
|
|
556
660
|
try {
|
|
557
661
|
if (fs.existsSync(gitignorePath)) {
|
|
558
662
|
const content = fs.readFileSync(gitignorePath, "utf-8");
|
|
559
|
-
if (!content.
|
|
560
|
-
const label = entry === ".devto.json" ? "
|
|
663
|
+
if (!content.split('\n').some(line => line.trim() === entry)) {
|
|
664
|
+
const label = entry === ".devto.json" ? "devto project config"
|
|
561
665
|
: entry === ".mcp.json" ? "MCP config (contains API keys)"
|
|
562
|
-
: "
|
|
666
|
+
: "devto credentials (contains API keys)";
|
|
563
667
|
fs.appendFileSync(gitignorePath, `\n# ${label}\n${entry}\n`);
|
|
564
668
|
}
|
|
565
669
|
}
|
|
566
670
|
else {
|
|
567
|
-
const label = entry === ".devto.json" ? "
|
|
671
|
+
const label = entry === ".devto.json" ? "devto project config"
|
|
568
672
|
: entry === ".mcp.json" ? "MCP config (contains API keys)"
|
|
569
|
-
: "
|
|
673
|
+
: "devto credentials (contains API keys)";
|
|
570
674
|
fs.writeFileSync(gitignorePath, `# ${label}\n${entry}\n`);
|
|
571
675
|
}
|
|
572
676
|
}
|
|
@@ -575,7 +679,7 @@ function ensureGitignore(entry) {
|
|
|
575
679
|
}
|
|
576
680
|
}
|
|
577
681
|
async function doctor() {
|
|
578
|
-
console.log("\
|
|
682
|
+
console.log("\ndevto Doctor — Connection Diagnostics\n");
|
|
579
683
|
const projectJson = requireProjectJson();
|
|
580
684
|
const resolved = (0, config_1.resolveConfig)();
|
|
581
685
|
const config = resolved?.config ?? null;
|
|
@@ -583,8 +687,8 @@ async function doctor() {
|
|
|
583
687
|
console.log(`OK (${path.join(projectJson.dir, ".devto.json")})`);
|
|
584
688
|
console.log(` Workspace: ${projectJson.config.workspaceUrl}`);
|
|
585
689
|
console.log(` Project: ${projectJson.config.projectKey} (${projectJson.config.provider})`);
|
|
586
|
-
// 1. Check
|
|
587
|
-
process.stdout.write("
|
|
690
|
+
// 1. Check devto API reachable
|
|
691
|
+
process.stdout.write("devto API reachable ... ");
|
|
588
692
|
const apiUrl = config?.api_url ?? (0, config_1.getApiUrl)();
|
|
589
693
|
try {
|
|
590
694
|
const res = await fetch(`${apiUrl}/api/v1/status`, {
|
|
@@ -665,15 +769,22 @@ async function doctor() {
|
|
|
665
769
|
console.log("SKIP (cannot reach API)");
|
|
666
770
|
}
|
|
667
771
|
}
|
|
668
|
-
// 4.
|
|
669
|
-
process.stdout.write("
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
772
|
+
// 4. AI call routing
|
|
773
|
+
process.stdout.write("AI calls ... ");
|
|
774
|
+
const hasLocalAnthropicKey = (() => {
|
|
775
|
+
try {
|
|
776
|
+
(0, config_1.getAnthropicKey)();
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
})();
|
|
783
|
+
if (hasLocalAnthropicKey) {
|
|
784
|
+
console.log("Expert Mode (your own Anthropic key)");
|
|
673
785
|
}
|
|
674
|
-
|
|
675
|
-
console.log("
|
|
676
|
-
console.log(" Fix: Run `devto config set anthropic-key sk-ant-xxxx`");
|
|
786
|
+
else {
|
|
787
|
+
console.log("via devto (no local key needed)");
|
|
677
788
|
}
|
|
678
789
|
// 5. Check .mcp.json in current directory
|
|
679
790
|
process.stdout.write("MCP config (.mcp.json) ... ");
|
|
@@ -701,7 +812,7 @@ async function doctor() {
|
|
|
701
812
|
console.log();
|
|
702
813
|
}
|
|
703
814
|
async function sync() {
|
|
704
|
-
console.log("\
|
|
815
|
+
console.log("\ndevto Sync — Workspace Configuration Discovery\n");
|
|
705
816
|
requireProjectJson();
|
|
706
817
|
const config = (0, config_1.readConfig)();
|
|
707
818
|
if (!config?.api_key) {
|
|
@@ -711,8 +822,8 @@ async function sync() {
|
|
|
711
822
|
const apiUrl = config.api_url;
|
|
712
823
|
process.stdout.write("Discovering workspace configuration... ");
|
|
713
824
|
try {
|
|
714
|
-
const res = await fetch(`${apiUrl}/api/v1/
|
|
715
|
-
method: "
|
|
825
|
+
const res = await fetch(`${apiUrl}/api/v1/project/summary`, {
|
|
826
|
+
method: "GET",
|
|
716
827
|
headers: {
|
|
717
828
|
"Content-Type": "application/json",
|
|
718
829
|
Authorization: `Bearer ${config.api_key}`,
|
|
@@ -767,14 +878,14 @@ async function configSet() {
|
|
|
767
878
|
}
|
|
768
879
|
function printHelp() {
|
|
769
880
|
console.log(`
|
|
770
|
-
|
|
881
|
+
devto CLI — AI-powered work management
|
|
771
882
|
|
|
772
883
|
Usage:
|
|
773
|
-
devto login Authenticate with your
|
|
884
|
+
devto login Authenticate with your devto API key
|
|
774
885
|
devto logout Clear global credentials (~/.devto)
|
|
775
886
|
devto status Show connection status and active project
|
|
776
|
-
devto init Link this project to
|
|
777
|
-
devto unlink Unlink this project from
|
|
887
|
+
devto init Link this project to devto
|
|
888
|
+
devto unlink Unlink this project from devto
|
|
778
889
|
devto doctor Test full connection chain
|
|
779
890
|
devto sync Re-sync workspace configuration
|
|
780
891
|
devto verbose Toggle verbose mode
|