@ahmedrowaihi/8n 0.5.17 → 0.5.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +129 -113
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -5,7 +5,7 @@ import path, { basename, delimiter, dirname, extname, join, normalize, relative,
5
5
  import fs, { appendFileSync, cpSync, existsSync, mkdirSync, promises, readFileSync, readdirSync, realpathSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
6
6
  import process$1, { cwd } from "node:process";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
- import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
8
+ import { readFile, readdir, rm, writeFile } from "node:fs/promises";
9
9
  import { homedir } from "node:os";
10
10
  import { parseTarGzip } from "nanotar";
11
11
  import { PassThrough } from "node:stream";
@@ -16,7 +16,7 @@ import v8 from "node:v8";
16
16
  import { format, inspect } from "node:util";
17
17
  import { z } from "zod";
18
18
  import { createInterface } from "node:readline/promises";
19
- import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
19
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
20
20
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21
21
 
22
22
  //#region \0rolldown/runtime.js
@@ -11600,7 +11600,7 @@ async function checkSelfUpdate() {
11600
11600
  });
11601
11601
  if (!res.ok) return;
11602
11602
  const { version: latest } = await res.json();
11603
- const current = "0.5.17";
11603
+ const current = "0.5.19";
11604
11604
  if (latest !== current) console.log(import_picocolors.default.yellow("⚠") + import_picocolors.default.dim(` new CLI version available: `) + import_picocolors.default.cyan(latest) + import_picocolors.default.dim(` (current: ${current}) — run `) + import_picocolors.default.cyan("npm i -g @ahmedrowaihi/8n") + import_picocolors.default.dim(" to update"));
11605
11605
  } catch {}
11606
11606
  }
@@ -11973,133 +11973,149 @@ async function prune() {
11973
11973
 
11974
11974
  //#endregion
11975
11975
  //#region src/commands/mcp.ts
11976
- async function walkMd(dir) {
11977
- const files = [];
11978
- async function walk(d) {
11979
- let entries;
11980
- try {
11981
- entries = await readdir(d, { withFileTypes: true });
11982
- } catch {
11983
- return;
11984
- }
11985
- for (const e of entries) {
11986
- const full = join(d, e.name);
11987
- if (e.isDirectory()) await walk(full);
11988
- else if (e.name.endsWith(".mdx") || e.name.endsWith(".md")) files.push(full);
11989
- }
11976
+ async function listMdFiles(dir) {
11977
+ let entries;
11978
+ try {
11979
+ entries = await readdir(dir, { withFileTypes: true });
11980
+ } catch {
11981
+ return [];
11990
11982
  }
11991
- await walk(dir);
11992
- return files;
11993
- }
11994
- function extractTitle(raw, fallback) {
11995
- const m = raw.match(/^---[\s\S]*?\ntitle:\s*(.+)/m);
11996
- return m ? m[1].trim().replace(/^["']|["']$/g, "") : fallback;
11997
- }
11998
- function safeAbsPath(base, rel) {
11999
- const abs = join(base, rel);
12000
- return abs.startsWith(base) ? abs : null;
11983
+ return entries.filter((e) => !e.isDirectory() && (e.name.endsWith(".mdx") || e.name.endsWith(".md"))).map((e) => e.name);
12001
11984
  }
12002
11985
  async function mcp() {
12003
- let contentDir = join(process.cwd(), "content");
12004
- try {
12005
- contentDir = (await resolveProject()).contentDir;
12006
- } catch {}
11986
+ const starterDir = findAnyCachedStarterDir();
11987
+ const mcpDir = starterDir ? join(starterDir, "content", "mcp", "en") : null;
12007
11988
  const server = new McpServer({
12008
11989
  name: "8n",
12009
- version: "0.5.17"
11990
+ version: "0.5.19"
12010
11991
  });
12011
- const knowledgeFiles = await walkMd(join(contentDir, "mcp"));
12012
- for (const file of knowledgeFiles) {
12013
- const name = basename(file, extname(file));
12014
- const uri = `8n://knowledge/${name}`;
12015
- server.registerResource(`knowledge-${name}`, uri, {
12016
- title: name,
12017
- mimeType: "text/markdown"
12018
- }, async () => {
12019
- return { contents: [{
12020
- uri,
12021
- text: await readFile(file, "utf-8"),
12022
- mimeType: "text/markdown"
12023
- }] };
12024
- });
12025
- }
12026
- server.registerResource("project-config", "8n://project/config", {
12027
- title: "8n.config.ts",
12028
- mimeType: "text/plain"
11992
+ server.registerTool("read_me", {
11993
+ description: "Returns how to use the 8n MCP tools. Call this BEFORE documenting anything with 8n.",
11994
+ inputSchema: {}
11995
+ }, async () => ({ content: [{
11996
+ type: "text",
11997
+ text: `# 8n MCP
11998
+
11999
+ Use these tools to learn how to write documentation for an 8n project before creating any content.
12000
+
12001
+ ## Tools
12002
+
12003
+ ### list_components
12004
+ Lists all available reference topics (config, structure, frontmatter, components).
12005
+ Call this first to discover what topics are available.
12006
+
12007
+ ### get_component
12008
+ Reads a specific topic by name (without extension).
12009
+ Example: get_component({ name: "components" }) returns all available MDX components with usage examples.
12010
+
12011
+ ## Workflow
12012
+ 1. Call list_components to see available topics
12013
+ 2. Call get_component for the topics relevant to your task
12014
+ 3. Use that knowledge to write correct MDX content for the user's project
12015
+ `
12016
+ }] }));
12017
+ server.registerTool("list_components", {
12018
+ description: "Lists all available 8n reference topics (config, structure, frontmatter, MDX components).",
12019
+ inputSchema: {}
12029
12020
  }, async () => {
12030
- const configPath = join(process.cwd(), "8n.config.ts");
12031
- return { contents: [{
12032
- uri: "8n://project/config",
12033
- text: existsSync(configPath) ? await readFile(configPath, "utf-8") : "No 8n.config.ts found in current directory.",
12034
- mimeType: "text/plain"
12021
+ if (!mcpDir || !existsSync(mcpDir)) return { content: [{
12022
+ type: "text",
12023
+ text: "8n starter not cached. Run `8n dev` first."
12035
12024
  }] };
12036
- });
12037
- const pagesTemplate = new ResourceTemplate("8n://pages/{path}", { list: async () => {
12038
- const pageFiles = (await walkMd(contentDir)).filter((f) => !relative(contentDir, f).startsWith("mcp/"));
12039
- return { resources: await Promise.all(pageFiles.map(async (f) => {
12040
- const rel = relative(contentDir, f);
12041
- const raw = await readFile(f, "utf-8");
12042
- return {
12043
- uri: `8n://pages/${rel}`,
12044
- name: extractTitle(raw, rel),
12045
- mimeType: "text/markdown"
12046
- };
12047
- })) };
12048
- } });
12049
- server.registerResource("pages", pagesTemplate, {
12050
- title: "Project pages",
12051
- mimeType: "text/markdown"
12052
- }, async (uri, { path }) => {
12053
- if (!path) return { contents: [{
12054
- uri: uri.href,
12055
- text: "Missing path.",
12056
- mimeType: "text/plain"
12025
+ return { content: [{
12026
+ type: "text",
12027
+ text: (await listMdFiles(mcpDir)).map((f) => basename(f, extname(f))).join("\n")
12057
12028
  }] };
12058
- const rel = Array.isArray(path) ? path[0] : path;
12059
- const abs = safeAbsPath(contentDir, rel);
12060
- if (!abs) return { contents: [{
12061
- uri: uri.href,
12062
- text: "Invalid path.",
12063
- mimeType: "text/plain"
12029
+ });
12030
+ server.registerTool("get_component", {
12031
+ description: "Reads a specific 8n reference topic by name. Use list_components first to discover available names.",
12032
+ inputSchema: { name: z.string().describe("Topic name without extension, e.g. components, config, structure, frontmatter") }
12033
+ }, async ({ name }) => {
12034
+ if (!mcpDir || !existsSync(mcpDir)) return { content: [{
12035
+ type: "text",
12036
+ text: "8n starter not cached. Run `8n dev` first."
12064
12037
  }] };
12065
- try {
12066
- const text = await readFile(abs, "utf-8");
12067
- return { contents: [{
12068
- uri: uri.href,
12069
- text,
12070
- mimeType: "text/markdown"
12071
- }] };
12072
- } catch {
12073
- return { contents: [{
12074
- uri: uri.href,
12075
- text: `File not found: ${rel}`,
12076
- mimeType: "text/plain"
12038
+ for (const ext of [".mdx", ".md"]) {
12039
+ const file = join(mcpDir, `${name}${ext}`);
12040
+ if (existsSync(file)) return { content: [{
12041
+ type: "text",
12042
+ text: await readFile(file, "utf-8")
12077
12043
  }] };
12078
12044
  }
12079
- });
12080
- server.tool("create_page", "Create a new MDX page in the content directory", {
12081
- path: z.string().describe("Path relative to content dir, e.g. docs/en/my-page.mdx"),
12082
- title: z.string().describe("Page title (used in frontmatter and sidebar)"),
12083
- description: z.string().optional().describe("Short description shown below the heading"),
12084
- body: z.string().optional().describe("MDX body content (everything after the frontmatter)")
12085
- }, async ({ path: p, title, description, body }) => {
12086
- const abs = safeAbsPath(contentDir, p);
12087
- if (!abs) return { content: [{
12045
+ return { content: [{
12088
12046
  type: "text",
12089
- text: "Invalid path."
12047
+ text: `Unknown topic: ${name}. Call list_components to see available topics.`
12090
12048
  }] };
12091
- if (existsSync(abs)) return { content: [{
12049
+ });
12050
+ server.registerTool("analyze", {
12051
+ description: "Runs static analysis on the user's content directory and reports issues that may break the documentation site.",
12052
+ inputSchema: {}
12053
+ }, async () => {
12054
+ let contentDir = join(process.cwd(), "content");
12055
+ let locales = ["en"];
12056
+ try {
12057
+ const project = await resolveProject();
12058
+ contentDir = project.contentDir;
12059
+ locales = project.config.locales ?? ["en"];
12060
+ } catch {}
12061
+ const issues = [];
12062
+ async function walkAll(dir) {
12063
+ const results = [];
12064
+ let entries;
12065
+ try {
12066
+ entries = await readdir(dir, { withFileTypes: true });
12067
+ } catch {
12068
+ return results;
12069
+ }
12070
+ for (const e of entries) {
12071
+ const full = join(dir, e.name);
12072
+ if (e.isDirectory()) results.push(...await walkAll(full));
12073
+ else results.push(full);
12074
+ }
12075
+ return results;
12076
+ }
12077
+ const allFiles = await walkAll(contentDir);
12078
+ const mdxFiles = allFiles.filter((f) => f.endsWith(".mdx") || f.endsWith(".md"));
12079
+ const metaFiles = allFiles.filter((f) => f.endsWith("meta.json"));
12080
+ for (const file of mdxFiles) {
12081
+ const raw = await readFile(file, "utf-8").catch(() => "");
12082
+ const rel = relative(contentDir, file);
12083
+ if (!raw.match(/^---[\s\S]*?\ntitle:/m)) issues.push(`Missing title in frontmatter: ${rel}`);
12084
+ if (rel.startsWith("changelog/")) {
12085
+ if (!raw.match(/\nversion:/)) issues.push(`Changelog page missing version: ${rel}`);
12086
+ if (!raw.match(/\ndate:/)) issues.push(`Changelog page missing date: ${rel}`);
12087
+ }
12088
+ const bodyMatch = raw.match(/^---[\s\S]*?---\s*([\s\S]*)$/m);
12089
+ if (bodyMatch && bodyMatch[1].trim() === "") issues.push(`Empty page body: ${rel}`);
12090
+ }
12091
+ for (const file of metaFiles) {
12092
+ const rel = relative(contentDir, file);
12093
+ const raw = await readFile(file, "utf-8").catch(() => "");
12094
+ let meta;
12095
+ try {
12096
+ meta = JSON.parse(raw);
12097
+ } catch {
12098
+ issues.push(`Malformed JSON: ${rel}`);
12099
+ continue;
12100
+ }
12101
+ if (meta.pages) {
12102
+ const dir = join(file, "..");
12103
+ for (const page of meta.pages) if (![".mdx", ".md"].some((ext) => existsSync(join(dir, `${page}${ext}`)))) issues.push(`meta.json references missing page "${page}": ${rel}`);
12104
+ }
12105
+ }
12106
+ const defaultLocale = locales[0];
12107
+ const otherLocales = locales.slice(1);
12108
+ for (const locale of otherLocales) {
12109
+ const defaultFiles = mdxFiles.filter((f) => f.includes(`/${defaultLocale}/`)).map((f) => relative(contentDir, f).replace(`${defaultLocale}/`, `${locale}/`));
12110
+ for (const expected of defaultFiles) if (!existsSync(join(contentDir, expected))) issues.push(`Missing ${locale} translation: ${expected}`);
12111
+ }
12112
+ if (issues.length === 0) return { content: [{
12092
12113
  type: "text",
12093
- text: `Already exists: ${p}`
12114
+ text: "✓ No issues found."
12094
12115
  }] };
12095
- const fm = ["---", `title: ${title}`];
12096
- if (description) fm.push(`description: ${description}`);
12097
- fm.push("---", "", body ?? "");
12098
- await mkdir(dirname(abs), { recursive: true });
12099
- await writeFile(abs, fm.join("\n"));
12100
12116
  return { content: [{
12101
12117
  type: "text",
12102
- text: `Created: ${p}`
12118
+ text: `Found ${issues.length} issue${issues.length === 1 ? "" : "s"}:\n\n${issues.map((i) => `- ${i}`).join("\n")}`
12103
12119
  }] };
12104
12120
  });
12105
12121
  const transport = new StdioServerTransport();
@@ -12108,7 +12124,7 @@ async function mcp() {
12108
12124
 
12109
12125
  //#endregion
12110
12126
  //#region src/index.ts
12111
- const program = new Command().name("8n").description("Run your 8n docs site").version("0.5.17").addOption(new Option("--debug").hideHelp()).hook("preAction", (cmd) => {
12127
+ const program = new Command().name("8n").description("Run your 8n docs site").version("0.5.19").addOption(new Option("--debug").hideHelp()).hook("preAction", (cmd) => {
12112
12128
  if (cmd.opts().debug) process.env.DEBUG_8N = "1";
12113
12129
  });
12114
12130
  program.command("init").description("Scaffold a new docs project in the current directory").action(init);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahmedrowaihi/8n",
3
- "version": "0.5.17",
3
+ "version": "0.5.19",
4
4
  "description": "Thmanyah Docs — run your docs site from your content directory",
5
5
  "type": "module",
6
6
  "bin": {