doc2mcp 0.1.17 → 0.1.18

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 CHANGED
@@ -29,6 +29,20 @@ npm install -g doc2mcp
29
29
  > Install with **`-g`** (global). The `doc2mcp` command only lands on your `PATH` when installed globally.
30
30
  > If you ran `npm i doc2mcp` (without `-g`) and see `command not found: doc2mcp`, either reinstall with `-g`
31
31
  > or run it through your package runner: `npx doc2mcp <docs-url>`.
32
+ >
33
+ > If `npm install -g doc2mcp` succeeds but `doc2mcp` is still `command not found`, your npm global bin folder is
34
+ > not on PATH. Run:
35
+ >
36
+ > ```bash
37
+ > echo 'export PATH="'$(npm prefix -g)'/bin:$PATH"' >> ~/.zshrc
38
+ > source ~/.zshrc
39
+ > ```
40
+ >
41
+ > Quick no-setup option:
42
+ >
43
+ > ```bash
44
+ > npx doc2mcp login
45
+ > ```
32
46
 
33
47
  Other package managers:
34
48
 
@@ -54,6 +68,9 @@ doc2mcp https://docs.stripe.com
54
68
 
55
69
  # 4. Chat with your docs without leaving the terminal
56
70
  doc2mcp chat
71
+
72
+ # Or paste a docs URL directly into chat mode
73
+ doc2mcp chat https://uagents.fetch.ai/docs
57
74
  ```
58
75
 
59
76
  That's it. The same hosted pipeline powers the [website](https://doc2mcp.site), so a project you
@@ -69,7 +86,7 @@ create in the CLI shows up in your dashboard and marketplace too.
69
86
  | [`doc2mcp whoami`](#doc2mcp-whoami) | Show the account you're signed in as |
70
87
  | [`doc2mcp list`](#doc2mcp-list) | List the MCP projects on your account |
71
88
  | [`doc2mcp install <projectId>`](#doc2mcp-install-projectid) | Install an existing MCP into your editors |
72
- | [`doc2mcp chat [projectId]`](#doc2mcp-chat-projectid) | Chat with your docs in the terminal (AI answers from your MCP) |
89
+ | [`doc2mcp chat [target]`](#doc2mcp-chat-target) | Chat with your docs in the terminal; target can be a project ID or docs URL |
73
90
  | `doc2mcp --version` | Print the installed CLI version |
74
91
  | `doc2mcp --help` | Show usage and all commands |
75
92
 
@@ -166,16 +183,19 @@ Existing config is merged, not overwritten.
166
183
 
167
184
  ---
168
185
 
169
- ### `doc2mcp chat [projectId]`
186
+ ### `doc2mcp chat [target]`
170
187
 
171
188
  Chat with your docs **right from the terminal**. doc2mcp answers natural-language questions from
172
189
  the crawled documentation — with cited sources — using the project's hosted MCP (the same
173
- `ask_documentation` tool your editor calls). This is the Playground experience, in your shell.
190
+ `ask_documentation` tool your editor calls). This is the Playground experience, in a Claude Code-style shell loop.
174
191
 
175
192
  ```bash
176
- # Interactive: choose a project, then ask anything
193
+ # Interactive: paste a docs URL, project ID, or choose an existing MCP
177
194
  doc2mcp chat
178
195
 
196
+ # Paste a docs URL directly: doc2mcp converts it, then starts chat
197
+ doc2mcp chat https://uagents.fetch.ai/docs
198
+
179
199
  # Skip the picker by passing a project ID
180
200
  doc2mcp chat prj_123abc
181
201
 
@@ -184,7 +204,7 @@ doc2mcp chat prj_123abc -m "How do I authenticate requests?"
184
204
  ```
185
205
 
186
206
  - With no arguments, you pick from your `ready` projects.
187
- - Type `/exit` (or press Esc) to leave an interactive session.
207
+ - Type `/exit` to leave an interactive session.
188
208
  - Each answer lists the source pages it used so you can verify it.
189
209
 
190
210
  ## Configuration
@@ -198,7 +218,8 @@ doc2mcp chat prj_123abc -m "How do I authenticate requests?"
198
218
 
199
219
  | Symptom | Fix |
200
220
  | --- | --- |
201
- | `command not found: doc2mcp` | You installed locally or your shell cached PATH. Reinstall with `npm i -g doc2mcp`, then run `hash -r` or open a new terminal. You can also use `npx doc2mcp <url>`. |
221
+ | `command not found: doc2mcp` | You installed locally or npm's global bin is not on PATH. Use `npx doc2mcp <url>`, or add `$(npm prefix -g)/bin` to PATH in `~/.zshrc`. |
222
+ | `pnpm add -g doc2mcp` says `ERR_PNPM_NO_GLOBAL_BIN_DIR` | Run `pnpm setup`, then `source ~/.zshrc`, then retry `pnpm add -g doc2mcp`. |
202
223
  | Browser doesn't open on `login` | Copy the printed URL into your browser manually, then approve. |
203
224
  | `login` can't reach the server | Confirm you're online; for self-hosting set `DOC2MCP_API_URL` to your instance. |
204
225
  | "Limit reached" | You've hit your plan's monthly conversion limit (shared across CLI and web). |
package/dist/index.js CHANGED
@@ -74,9 +74,10 @@ async function runWhoami() {
74
74
  }
75
75
 
76
76
  // src/commands/chat.ts
77
- import { cancel, intro, isCancel, outro, select, text } from "@clack/prompts";
77
+ import { createInterface } from "readline/promises";
78
+ import { stdin as input, stdout as output } from "process";
78
79
  import ora2 from "ora";
79
- import pc4 from "picocolors";
80
+ import pc6 from "picocolors";
80
81
 
81
82
  // src/api.ts
82
83
  import pc2 from "picocolors";
@@ -90,14 +91,14 @@ var ApiError = class extends Error {
90
91
  }
91
92
  };
92
93
  async function parseJson(response) {
93
- const text2 = await response.text();
94
- if (!text2) {
94
+ const text = await response.text();
95
+ if (!text) {
95
96
  return null;
96
97
  }
97
98
  try {
98
- return JSON.parse(text2);
99
+ return JSON.parse(text);
99
100
  } catch {
100
- return { raw: text2 };
101
+ return { raw: text };
101
102
  }
102
103
  }
103
104
  async function apiFetch(path, options = {}) {
@@ -138,6 +139,9 @@ function printError(error) {
138
139
  `);
139
140
  }
140
141
 
142
+ // src/commands/convert.ts
143
+ import pc5 from "picocolors";
144
+
141
145
  // src/commands/login.ts
142
146
  import open from "open";
143
147
  import ora from "ora";
@@ -232,151 +236,9 @@ async function ensureLoggedIn() {
232
236
  }
233
237
  }
234
238
 
235
- // src/commands/chat.ts
236
- async function pickProject(explicitId) {
237
- if (explicitId) {
238
- return await apiFetch(`/api/cli/projects/${explicitId}`);
239
- }
240
- const data = await apiFetch(
241
- "/api/cli/projects"
242
- );
243
- const ready = data.projects.filter((p) => p.status === "ready");
244
- if (ready.length === 0) {
245
- process.stdout.write(
246
- `${pc4.yellow("No ready MCP projects yet.")} Create one: ${pc4.bold("doc2mcp <docs-url>")}
247
- `
248
- );
249
- return null;
250
- }
251
- const choice = await select({
252
- message: "Which docs do you want to chat with?",
253
- options: ready.map((p) => ({
254
- value: p.id,
255
- label: p.name,
256
- hint: p.sourceUrl ?? void 0
257
- }))
258
- });
259
- if (isCancel(choice)) {
260
- cancel("Cancelled.");
261
- return null;
262
- }
263
- return await apiFetch(`/api/cli/projects/${choice}`);
264
- }
265
- async function askDocs(mcp, question) {
266
- const response = await fetch(mcp.url, {
267
- method: "POST",
268
- headers: {
269
- "Content-Type": "application/json",
270
- Authorization: `Bearer ${mcp.token}`
271
- },
272
- body: JSON.stringify({
273
- jsonrpc: "2.0",
274
- id: 1,
275
- method: "tools/call",
276
- params: { name: "ask_documentation", arguments: { question } }
277
- })
278
- });
279
- const payload = await response.json();
280
- if (!response.ok || payload.error) {
281
- throw new ApiError(
282
- payload.error?.message ?? `MCP request failed (${response.status})`,
283
- response.status,
284
- payload
285
- );
286
- }
287
- const raw = payload.result?.content?.[0]?.text ?? "";
288
- try {
289
- return JSON.parse(raw);
290
- } catch {
291
- return { question, answer: raw };
292
- }
293
- }
294
- function renderAnswer(answer) {
295
- process.stdout.write(`
296
- ${pc4.cyan("\u25C6")} ${answer.answer.trim()}
297
- `);
298
- if (answer.sources && answer.sources.length > 0) {
299
- process.stdout.write(`
300
- ${pc4.dim("Sources:")}
301
- `);
302
- for (const source of answer.sources.slice(0, 6)) {
303
- process.stdout.write(` ${pc4.dim("\u2022")} ${source.title} ${pc4.dim(source.url)}
304
- `);
305
- }
306
- }
307
- process.stdout.write("\n");
308
- }
309
- async function answerOnce(mcp, question) {
310
- const spinner = ora2("Thinking\u2026").start();
311
- try {
312
- const answer = await askDocs(mcp, question);
313
- spinner.stop();
314
- renderAnswer(answer);
315
- } catch (error) {
316
- spinner.fail("Failed to get an answer");
317
- printError(error);
318
- }
319
- }
320
- async function runChat(projectId, options = {}) {
321
- try {
322
- await ensureLoggedIn();
323
- const detail = await pickProject(projectId);
324
- if (!detail) {
325
- return;
326
- }
327
- if (!detail.mcp) {
328
- process.stderr.write(
329
- `${pc4.red("That project is not ready yet.")} Check: ${pc4.bold("doc2mcp list")}
330
- `
331
- );
332
- process.exitCode = 1;
333
- return;
334
- }
335
- const { mcp } = detail;
336
- if (options.message) {
337
- await answerOnce(mcp, options.message);
338
- return;
339
- }
340
- intro(
341
- `${pc4.bold(`Chatting with ${detail.project.name}`)} ${pc4.dim("\u2014 ask anything about these docs")}`
342
- );
343
- process.stdout.write(
344
- `${pc4.dim("Type your question. Use /exit to leave.")}
345
- `
346
- );
347
- let active = true;
348
- while (active) {
349
- const question = await text({
350
- message: "You",
351
- placeholder: "How do I authenticate requests?"
352
- });
353
- if (isCancel(question)) {
354
- active = false;
355
- break;
356
- }
357
- const trimmed = String(question).trim();
358
- if (!trimmed) {
359
- continue;
360
- }
361
- if (trimmed === "/exit" || trimmed === "/quit") {
362
- active = false;
363
- break;
364
- }
365
- await answerOnce(mcp, trimmed);
366
- }
367
- outro(pc4.dim("Bye \u2014 your docs MCP stays live for your editor."));
368
- } catch (error) {
369
- printError(error);
370
- process.exitCode = 1;
371
- }
372
- }
373
-
374
- // src/commands/convert.ts
375
- import pc6 from "picocolors";
376
-
377
239
  // src/commands/install.ts
378
240
  import { confirm, multiselect } from "@clack/prompts";
379
- import pc5 from "picocolors";
241
+ import pc4 from "picocolors";
380
242
 
381
243
  // src/installers/detect.ts
382
244
  import { access } from "fs/promises";
@@ -563,8 +425,8 @@ async function promptInstall(install) {
563
425
  }
564
426
  await installToClient(client.id, client.configPath, install);
565
427
  process.stdout.write(
566
- `${pc5.green("Installed")} ${client.label}
567
- ${pc5.dim(client.configPath)}
428
+ `${pc4.green("Installed")} ${client.label}
429
+ ${pc4.dim(client.configPath)}
568
430
  `
569
431
  );
570
432
  }
@@ -574,7 +436,7 @@ async function runInstallCommand(projectId) {
574
436
  await ensureLoggedIn();
575
437
  const detail = await apiFetch(`/api/cli/projects/${projectId}`);
576
438
  if (!detail.install) {
577
- process.stderr.write(`${pc5.red("Project is not ready or missing install bundle.")}
439
+ process.stderr.write(`${pc4.red("Project is not ready or missing install bundle.")}
578
440
  `);
579
441
  process.exitCode = 1;
580
442
  return;
@@ -595,66 +457,72 @@ function sleep2(ms) {
595
457
  function printStatus(detail) {
596
458
  const { project } = detail;
597
459
  process.stdout.write(
598
- `\r${pc6.cyan("Status:")} ${project.status.padEnd(12)} ${pc6.dim(project.name)}`
460
+ `\r${pc5.cyan("Status:")} ${project.status.padEnd(12)} ${pc5.dim(project.name)}`
599
461
  );
600
462
  }
601
- async function runConvert(sourceUrl) {
602
- try {
603
- await ensureLoggedIn();
604
- process.stdout.write(`${pc6.bold("Converting")} ${sourceUrl}
463
+ async function convertUrlToProject(sourceUrl, options = { offerInstall: true }) {
464
+ await ensureLoggedIn();
465
+ process.stdout.write(`${pc5.bold("Converting")} ${sourceUrl}
605
466
  `);
606
- const created = await apiFetch("/api/cli/convert", {
607
- method: "POST",
608
- body: JSON.stringify({ sourceUrl })
609
- });
610
- process.stdout.write(`${pc6.dim("Project:")} ${created.id}
467
+ const created = await apiFetch("/api/cli/convert", {
468
+ method: "POST",
469
+ body: JSON.stringify({ sourceUrl })
470
+ });
471
+ process.stdout.write(`${pc5.dim("Project:")} ${created.id}
611
472
  `);
612
- let delayMs = 2e3;
613
- const terminal = /* @__PURE__ */ new Set(["ready", "error"]);
614
- while (true) {
615
- const detail = await apiFetch(
616
- `/api/cli/projects/${created.id}`
617
- );
618
- printStatus(detail);
619
- if (terminal.has(detail.project.status)) {
620
- process.stdout.write("\n");
621
- break;
622
- }
623
- await sleep2(delayMs);
624
- delayMs = Math.min(delayMs + 1e3, 1e4);
625
- }
626
- const finalDetail = await apiFetch(
473
+ let delayMs = 2e3;
474
+ const terminal = /* @__PURE__ */ new Set(["ready", "error"]);
475
+ while (true) {
476
+ const detail = await apiFetch(
627
477
  `/api/cli/projects/${created.id}`
628
478
  );
629
- if (finalDetail.project.status === "error") {
630
- const lastLog = finalDetail.project.logs.at(-1);
631
- process.stderr.write(
632
- `${pc6.red("Conversion failed.")}${lastLog ? ` ${lastLog.message}` : ""}
633
- `
634
- );
635
- process.exitCode = 1;
636
- return;
479
+ printStatus(detail);
480
+ if (terminal.has(detail.project.status)) {
481
+ process.stdout.write("\n");
482
+ break;
637
483
  }
638
- if (!finalDetail.mcp || !finalDetail.install) {
639
- process.stderr.write(`${pc6.red("MCP ready but missing install bundle.")}
484
+ await sleep2(delayMs);
485
+ delayMs = Math.min(delayMs + 1e3, 1e4);
486
+ }
487
+ const finalDetail = await apiFetch(
488
+ `/api/cli/projects/${created.id}`
489
+ );
490
+ if (finalDetail.project.status === "error") {
491
+ const lastLog = finalDetail.project.logs.at(-1);
492
+ process.stderr.write(
493
+ `${pc5.red("Conversion failed.")}${lastLog ? ` ${lastLog.message}` : ""}
494
+ `
495
+ );
496
+ process.exitCode = 1;
497
+ return null;
498
+ }
499
+ if (!finalDetail.mcp || !finalDetail.install) {
500
+ process.stderr.write(`${pc5.red("MCP ready but missing install bundle.")}
640
501
  `);
641
- process.exitCode = 1;
642
- return;
643
- }
644
- process.stdout.write(`
645
- ${pc6.green("MCP ready")}
502
+ process.exitCode = 1;
503
+ return null;
504
+ }
505
+ process.stdout.write(`
506
+ ${pc5.green("MCP ready")}
646
507
  `);
647
- process.stdout.write(`${pc6.bold("Server:")} ${finalDetail.mcp.serverName}
508
+ process.stdout.write(`${pc5.bold("Server:")} ${finalDetail.mcp.serverName}
648
509
  `);
649
- process.stdout.write(`${pc6.bold("URL:")} ${finalDetail.mcp.url}
510
+ process.stdout.write(`${pc5.bold("URL:")} ${finalDetail.mcp.url}
650
511
  `);
651
- process.stdout.write(`${pc6.bold("Token:")} ${finalDetail.mcp.token}
512
+ process.stdout.write(`${pc5.bold("Token:")} ${finalDetail.mcp.token}
652
513
  `);
653
- process.stdout.write(
654
- `${pc6.dim("Also listed in the doc2mcp marketplace when ready.")}
514
+ process.stdout.write(
515
+ `${pc5.dim("Also listed in the doc2mcp marketplace when ready.")}
655
516
  `
656
- );
517
+ );
518
+ if (options.offerInstall) {
657
519
  await promptInstall(finalDetail.install);
520
+ }
521
+ return finalDetail;
522
+ }
523
+ async function runConvert(sourceUrl) {
524
+ try {
525
+ await convertUrlToProject(sourceUrl, { offerInstall: true });
658
526
  } catch (error) {
659
527
  printError(error);
660
528
  process.exitCode = 1;
@@ -665,18 +533,225 @@ async function runList() {
665
533
  await ensureLoggedIn();
666
534
  const data = await apiFetch("/api/cli/projects");
667
535
  if (data.projects.length === 0) {
668
- process.stdout.write(`${pc6.dim("No projects yet.")}
536
+ process.stdout.write(`${pc5.dim("No projects yet.")}
669
537
  `);
670
538
  return;
671
539
  }
672
540
  for (const project of data.projects) {
673
541
  process.stdout.write(
674
- `${pc6.bold(project.name)} ${pc6.dim(`[${project.status}]`)} ${project.source}
542
+ `${pc5.bold(project.name)} ${pc5.dim(`[${project.status}]`)} ${project.source}
543
+ `
544
+ );
545
+ process.stdout.write(` ${pc5.dim(project.id)} ${project.sourceUrl ?? ""}
546
+ `);
547
+ }
548
+ } catch (error) {
549
+ printError(error);
550
+ process.exitCode = 1;
551
+ }
552
+ }
553
+
554
+ // src/commands/chat.ts
555
+ function isDocsUrl(value) {
556
+ try {
557
+ const url = new URL(value);
558
+ return url.protocol === "http:" || url.protocol === "https:";
559
+ } catch {
560
+ return false;
561
+ }
562
+ }
563
+ function createPrompt() {
564
+ return createInterface({ input, output });
565
+ }
566
+ async function readLine(prompt) {
567
+ const rl = createPrompt();
568
+ try {
569
+ const answer = await rl.question(prompt);
570
+ return answer.trim();
571
+ } catch {
572
+ return null;
573
+ } finally {
574
+ rl.close();
575
+ }
576
+ }
577
+ async function listReadyProjects() {
578
+ const data = await apiFetch(
579
+ "/api/cli/projects"
580
+ );
581
+ return data.projects.filter((p) => p.status === "ready");
582
+ }
583
+ async function pickExistingProject() {
584
+ const ready = await listReadyProjects();
585
+ if (ready.length === 0) {
586
+ process.stdout.write(
587
+ `${pc6.yellow("No ready MCP projects yet.")} Paste a docs URL to create one.
588
+ `
589
+ );
590
+ return null;
591
+ }
592
+ process.stdout.write(`${pc6.dim("Ready MCPs")}
593
+ `);
594
+ ready.forEach((p, index) => {
595
+ process.stdout.write(
596
+ ` ${pc6.cyan(String(index + 1).padStart(2, " "))}. ${pc6.bold(p.name)} ${pc6.dim(p.sourceUrl ?? p.id)}
597
+ `
598
+ );
599
+ });
600
+ const choice = await readLine(
601
+ `${pc6.bold(">")} choose number, paste URL, or paste project id: `
602
+ );
603
+ if (!choice) {
604
+ return null;
605
+ }
606
+ if (isDocsUrl(choice)) {
607
+ return await convertUrlToProject(choice, { offerInstall: false });
608
+ }
609
+ const chosenIndex = Number(choice);
610
+ if (Number.isInteger(chosenIndex) && chosenIndex > 0) {
611
+ const project = ready.at(chosenIndex - 1);
612
+ if (project) {
613
+ return await apiFetch(`/api/cli/projects/${project.id}`);
614
+ }
615
+ }
616
+ return await apiFetch(`/api/cli/projects/${choice}`);
617
+ }
618
+ async function resolveProject(target) {
619
+ if (target) {
620
+ if (isDocsUrl(target)) {
621
+ return await convertUrlToProject(target, { offerInstall: false });
622
+ }
623
+ return await apiFetch(`/api/cli/projects/${target}`);
624
+ }
625
+ process.stdout.write(`${pc6.bold("doc2mcp chat")}
626
+ `);
627
+ process.stdout.write(
628
+ `${pc6.dim("Paste a docs URL to create a new MCP, paste a project id, or press Enter to choose existing.")}
629
+
630
+ `
631
+ );
632
+ const first = await readLine(`${pc6.bold(">")} docs url or project id: `);
633
+ if (!first) {
634
+ return await pickExistingProject();
635
+ }
636
+ if (isDocsUrl(first)) {
637
+ return await convertUrlToProject(first, { offerInstall: false });
638
+ }
639
+ return await apiFetch(`/api/cli/projects/${first}`);
640
+ }
641
+ async function askDocs(mcp, question) {
642
+ const response = await fetch(mcp.url, {
643
+ method: "POST",
644
+ headers: {
645
+ "Content-Type": "application/json",
646
+ Authorization: `Bearer ${mcp.token}`
647
+ },
648
+ body: JSON.stringify({
649
+ jsonrpc: "2.0",
650
+ id: 1,
651
+ method: "tools/call",
652
+ params: { name: "ask_documentation", arguments: { question } }
653
+ })
654
+ });
655
+ const payload = await response.json();
656
+ if (!response.ok || payload.error) {
657
+ throw new ApiError(
658
+ payload.error?.message ?? `MCP request failed (${response.status})`,
659
+ response.status,
660
+ payload
661
+ );
662
+ }
663
+ const raw = payload.result?.content?.[0]?.text ?? "";
664
+ try {
665
+ return JSON.parse(raw);
666
+ } catch {
667
+ return { question, answer: raw };
668
+ }
669
+ }
670
+ function renderAnswer(answer) {
671
+ process.stdout.write(`
672
+ ${pc6.cyan("\u256D\u2500 doc2mcp")}
673
+ `);
674
+ for (const line of answer.answer.trim().split("\n")) {
675
+ process.stdout.write(`${pc6.cyan("\u2502")} ${line}
676
+ `);
677
+ }
678
+ if (answer.sources && answer.sources.length > 0) {
679
+ process.stdout.write(`${pc6.cyan("\u2502")}
680
+ ${pc6.cyan("\u2502")} ${pc6.dim("Sources:")}
681
+ `);
682
+ for (const source of answer.sources.slice(0, 6)) {
683
+ process.stdout.write(
684
+ `${pc6.cyan("\u2502")} ${pc6.dim("\u2022")} ${source.title} ${pc6.dim(source.url)}
685
+ `
686
+ );
687
+ }
688
+ }
689
+ process.stdout.write(`${pc6.cyan("\u2570")}
690
+
691
+ `);
692
+ }
693
+ async function answerOnce(mcp, question) {
694
+ const spinner = ora2("Thinking\u2026").start();
695
+ try {
696
+ const answer = await askDocs(mcp, question);
697
+ spinner.stop();
698
+ renderAnswer(answer);
699
+ } catch (error) {
700
+ spinner.fail("Failed to get an answer");
701
+ printError(error);
702
+ }
703
+ }
704
+ async function runChat(target, options = {}) {
705
+ try {
706
+ await ensureLoggedIn();
707
+ const detail = await resolveProject(target);
708
+ if (!detail) {
709
+ return;
710
+ }
711
+ if (!detail.mcp) {
712
+ process.stderr.write(
713
+ `${pc6.red("That project is not ready yet.")} Check: ${pc6.bold("doc2mcp list")}
675
714
  `
676
715
  );
677
- process.stdout.write(` ${pc6.dim(project.id)} ${project.sourceUrl ?? ""}
716
+ process.exitCode = 1;
717
+ return;
718
+ }
719
+ const { mcp } = detail;
720
+ if (options.message) {
721
+ await answerOnce(mcp, options.message);
722
+ return;
723
+ }
724
+ process.stdout.write(
725
+ `
726
+ ${pc6.cyan("\u256D\u2500")} ${pc6.bold(`Chatting with ${detail.project.name}`)}
727
+ `
728
+ );
729
+ process.stdout.write(
730
+ `${pc6.cyan("\u2502")} ${pc6.dim("Ask anything about these docs. Type /exit to leave.")}
731
+ `
732
+ );
733
+ process.stdout.write(`${pc6.cyan("\u2570")}
734
+
678
735
  `);
736
+ let active = true;
737
+ while (active) {
738
+ const question = await readLine(`${pc6.bold(">")} `);
739
+ if (question === null) {
740
+ active = false;
741
+ break;
742
+ }
743
+ const trimmed = question.trim();
744
+ if (!trimmed) {
745
+ continue;
746
+ }
747
+ if (trimmed === "/exit" || trimmed === "/quit") {
748
+ active = false;
749
+ break;
750
+ }
751
+ await answerOnce(mcp, trimmed);
679
752
  }
753
+ process.stdout.write(`${pc6.dim("Bye \u2014 your docs MCP stays live for your editor.")}
754
+ `);
680
755
  } catch (error) {
681
756
  printError(error);
682
757
  process.exitCode = 1;
@@ -685,7 +760,7 @@ async function runList() {
685
760
 
686
761
  // src/index.ts
687
762
  var program = new Command();
688
- program.name("doc2mcp").description("Generate documentation MCP servers from your terminal").version("0.1.17", "-v, --version", "Print the installed CLI version");
763
+ program.name("doc2mcp").description("Generate documentation MCP servers from your terminal").version("0.1.18", "-v, --version", "Print the installed CLI version");
689
764
  program.command("login").description("Authorize the CLI via browser").action(async () => {
690
765
  await runLogin();
691
766
  });
@@ -701,8 +776,8 @@ program.command("list").description("List your MCP projects").action(async () =>
701
776
  program.command("install <projectId>").description("Install an existing MCP into Cursor, VS Code, Claude, or Windsurf").action(async (projectId) => {
702
777
  await runInstallCommand(projectId);
703
778
  });
704
- program.command("chat [projectId]").description("Chat with your docs in the terminal (AI answers from your MCP)").option("-m, --message <text>", "Ask a single question and exit").action(async (projectId, options) => {
705
- await runChat(projectId, options);
779
+ program.command("chat [target]").description("Chat with docs in the terminal; target can be a project id or docs URL").option("-m, --message <text>", "Ask a single question and exit").action(async (target, options) => {
780
+ await runChat(target, options);
706
781
  });
707
782
  program.argument("[url]", "Documentation URL to convert").action(async (url) => {
708
783
  if (!url) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc2mcp",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Turn any documentation site into a hosted MCP server from your terminal — for Cursor, Claude, VS Code, Windsurf, and OpenAI agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "files": [
19
19
  "assets",
20
20
  "dist",
21
+ "scripts",
21
22
  "README.md"
22
23
  ],
23
24
  "engines": {
@@ -26,6 +27,7 @@
26
27
  "scripts": {
27
28
  "build": "tsup",
28
29
  "dev": "tsup --watch",
30
+ "postinstall": "node ./scripts/postinstall.js",
29
31
  "prepublishOnly": "pnpm build"
30
32
  },
31
33
  "keywords": [
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from "node:child_process";
4
+ import path from "node:path";
5
+
6
+ function isGlobalInstall() {
7
+ return (
8
+ process.env.npm_config_global === "true" ||
9
+ process.env.npm_config_location === "global"
10
+ );
11
+ }
12
+
13
+ function npmGlobalBin() {
14
+ try {
15
+ const prefix = execFileSync("npm", ["prefix", "-g"], {
16
+ encoding: "utf8",
17
+ stdio: ["ignore", "pipe", "ignore"],
18
+ }).trim();
19
+ return path.join(prefix, "bin");
20
+ } catch {
21
+ return "";
22
+ }
23
+ }
24
+
25
+ if (isGlobalInstall()) {
26
+ const binDir = npmGlobalBin();
27
+ const pathEntries = (process.env.PATH || "").split(path.delimiter);
28
+ if (binDir && !pathEntries.includes(binDir)) {
29
+ process.stdout.write(`
30
+ doc2mcp installed, but npm's global bin is not on your PATH.
31
+
32
+ Run this once for zsh:
33
+ echo 'export PATH="${binDir}:$PATH"' >> ~/.zshrc
34
+ source ~/.zshrc
35
+
36
+ Then try:
37
+ doc2mcp login
38
+
39
+ Quick alternative (no PATH setup):
40
+ npx doc2mcp login
41
+
42
+ For pnpm global installs, run:
43
+ pnpm setup
44
+ source ~/.zshrc
45
+
46
+ `);
47
+ }
48
+ }