coding-friend-cli 1.0.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/dist/chunk-6CGGT2FD.js +32 -0
- package/dist/chunk-6DUFTBTO.js +14 -0
- package/dist/chunk-AQXTNLQD.js +39 -0
- package/dist/chunk-HRVSKMNA.js +31 -0
- package/dist/chunk-IUTXHCP7.js +28 -0
- package/dist/chunk-KZT4AFDW.js +44 -0
- package/dist/chunk-VHZQ6KEU.js +73 -0
- package/dist/host-3GAEZKKJ.js +83 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +34 -0
- package/dist/init-ONRXFOZ5.js +431 -0
- package/dist/json-2XS56OJY.js +10 -0
- package/dist/mcp-LMMIFH4B.js +104 -0
- package/dist/postinstall.d.ts +1 -0
- package/dist/postinstall.js +8 -0
- package/dist/statusline-7D6YU5YM.js +64 -0
- package/dist/update-K5PYOB52.js +160 -0
- package/lib/learn-host/next-env.d.ts +6 -0
- package/lib/learn-host/next.config.ts +9 -0
- package/lib/learn-host/package-lock.json +3943 -0
- package/lib/learn-host/package.json +31 -0
- package/lib/learn-host/postcss.config.mjs +7 -0
- package/lib/learn-host/scripts/build-search-index.ts +11 -0
- package/lib/learn-host/src/app/[category]/[slug]/page.tsx +61 -0
- package/lib/learn-host/src/app/[category]/page.tsx +35 -0
- package/lib/learn-host/src/app/globals.css +31 -0
- package/lib/learn-host/src/app/layout.tsx +32 -0
- package/lib/learn-host/src/app/page.tsx +63 -0
- package/lib/learn-host/src/app/search/page.tsx +94 -0
- package/lib/learn-host/src/app/search/search-index.json +42 -0
- package/lib/learn-host/src/components/Breadcrumbs.tsx +28 -0
- package/lib/learn-host/src/components/DocCard.tsx +32 -0
- package/lib/learn-host/src/components/MarkdownRenderer.tsx +13 -0
- package/lib/learn-host/src/components/MobileNav.tsx +56 -0
- package/lib/learn-host/src/components/SearchBar.tsx +36 -0
- package/lib/learn-host/src/components/Sidebar.tsx +44 -0
- package/lib/learn-host/src/components/TagBadge.tsx +12 -0
- package/lib/learn-host/src/components/ThemeToggle.tsx +30 -0
- package/lib/learn-host/src/lib/docs.ts +113 -0
- package/lib/learn-host/src/lib/search.ts +27 -0
- package/lib/learn-host/src/lib/types.ts +31 -0
- package/lib/learn-host/tsconfig.json +21 -0
- package/lib/learn-mcp/package-lock.json +1829 -0
- package/lib/learn-mcp/package.json +24 -0
- package/lib/learn-mcp/src/bin/learn-mcp.ts +2 -0
- package/lib/learn-mcp/src/index.ts +17 -0
- package/lib/learn-mcp/src/lib/docs.ts +199 -0
- package/lib/learn-mcp/src/lib/knowledge.ts +95 -0
- package/lib/learn-mcp/src/lib/types.ts +36 -0
- package/lib/learn-mcp/src/server.ts +22 -0
- package/lib/learn-mcp/src/tools/create-doc.ts +29 -0
- package/lib/learn-mcp/src/tools/get-review-list.ts +29 -0
- package/lib/learn-mcp/src/tools/improve-doc.ts +95 -0
- package/lib/learn-mcp/src/tools/list-categories.ts +19 -0
- package/lib/learn-mcp/src/tools/list-docs.ts +30 -0
- package/lib/learn-mcp/src/tools/read-doc.ts +29 -0
- package/lib/learn-mcp/src/tools/search-docs.ts +23 -0
- package/lib/learn-mcp/src/tools/track-knowledge.ts +35 -0
- package/lib/learn-mcp/src/tools/update-doc.ts +43 -0
- package/lib/learn-mcp/tsconfig.json +15 -0
- package/package.json +47 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTheme } from "next-themes";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
export default function ThemeToggle() {
|
|
7
|
+
const { theme, setTheme } = useTheme();
|
|
8
|
+
const [mounted, setMounted] = useState(false);
|
|
9
|
+
|
|
10
|
+
useEffect(() => setMounted(true), []);
|
|
11
|
+
if (!mounted) return <div className="w-9 h-9" />;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<button
|
|
15
|
+
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
16
|
+
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
17
|
+
aria-label="Toggle theme"
|
|
18
|
+
>
|
|
19
|
+
{theme === "dark" ? (
|
|
20
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
21
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
22
|
+
</svg>
|
|
23
|
+
) : (
|
|
24
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
25
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
|
26
|
+
</svg>
|
|
27
|
+
)}
|
|
28
|
+
</button>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import type { CategoryInfo, Doc, DocFrontmatter, DocMeta } from "./types";
|
|
5
|
+
|
|
6
|
+
function getDocsDir(): string {
|
|
7
|
+
const raw = process.env.DOCS_DIR || path.join(process.cwd(), "../../docs/learn");
|
|
8
|
+
return path.resolve(raw);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isMarkdownDoc(fileName: string): boolean {
|
|
12
|
+
return fileName.endsWith(".md") && fileName !== "README.md";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseFrontmatter(raw: matter.GrayMatterFile<string>): DocFrontmatter {
|
|
16
|
+
const d = raw.data;
|
|
17
|
+
return {
|
|
18
|
+
title: String(d.title ?? "Untitled"),
|
|
19
|
+
category: String(d.category ?? ""),
|
|
20
|
+
tags: Array.isArray(d.tags) ? d.tags.map(String) : [],
|
|
21
|
+
created: String(d.created ?? ""),
|
|
22
|
+
updated: String(d.updated ?? ""),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeExcerpt(content: string, maxLen = 160): string {
|
|
27
|
+
const text = content
|
|
28
|
+
.replace(/^#+\s.*/gm, "")
|
|
29
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
30
|
+
.replace(/\n{2,}/g, " ")
|
|
31
|
+
.replace(/\n/g, " ")
|
|
32
|
+
.trim();
|
|
33
|
+
return text.length > maxLen ? text.slice(0, maxLen) + "..." : text;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getAllCategories(): CategoryInfo[] {
|
|
37
|
+
const docsDir = getDocsDir();
|
|
38
|
+
if (!fs.existsSync(docsDir)) return [];
|
|
39
|
+
return fs
|
|
40
|
+
.readdirSync(docsDir, { withFileTypes: true })
|
|
41
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith("."))
|
|
42
|
+
.map((d) => {
|
|
43
|
+
const catPath = path.join(docsDir, d.name);
|
|
44
|
+
const count = fs
|
|
45
|
+
.readdirSync(catPath)
|
|
46
|
+
.filter(isMarkdownDoc).length;
|
|
47
|
+
return { name: d.name, docCount: count };
|
|
48
|
+
})
|
|
49
|
+
.filter((c) => c.docCount > 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getAllDocs(): DocMeta[] {
|
|
53
|
+
const docsDir = getDocsDir();
|
|
54
|
+
const docs: DocMeta[] = [];
|
|
55
|
+
const categories = getAllCategories();
|
|
56
|
+
|
|
57
|
+
for (const cat of categories) {
|
|
58
|
+
const catPath = path.join(docsDir, cat.name);
|
|
59
|
+
const files = fs.readdirSync(catPath).filter(isMarkdownDoc);
|
|
60
|
+
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const filePath = path.join(catPath, file);
|
|
63
|
+
const raw = matter(fs.readFileSync(filePath, "utf-8"));
|
|
64
|
+
const frontmatter = parseFrontmatter(raw);
|
|
65
|
+
|
|
66
|
+
docs.push({
|
|
67
|
+
slug: path.basename(file, ".md"),
|
|
68
|
+
category: cat.name,
|
|
69
|
+
frontmatter: { ...frontmatter, category: cat.name },
|
|
70
|
+
excerpt: makeExcerpt(raw.content),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return docs.sort(
|
|
76
|
+
(a, b) =>
|
|
77
|
+
new Date(b.frontmatter.updated || b.frontmatter.created).getTime() -
|
|
78
|
+
new Date(a.frontmatter.updated || a.frontmatter.created).getTime(),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getDocsByCategory(category: string): DocMeta[] {
|
|
83
|
+
return getAllDocs().filter((d) => d.category === category);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getDocBySlug(category: string, slug: string): Doc | null {
|
|
87
|
+
const docsDir = getDocsDir();
|
|
88
|
+
const filePath = path.join(docsDir, category, `${slug}.md`);
|
|
89
|
+
if (!fs.existsSync(filePath)) return null;
|
|
90
|
+
|
|
91
|
+
const raw = matter(fs.readFileSync(filePath, "utf-8"));
|
|
92
|
+
const frontmatter = parseFrontmatter(raw);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
slug,
|
|
96
|
+
category,
|
|
97
|
+
frontmatter: { ...frontmatter, category },
|
|
98
|
+
excerpt: makeExcerpt(raw.content),
|
|
99
|
+
content: raw.content,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getAllTags(): { tag: string; count: number }[] {
|
|
104
|
+
const tagMap = new Map<string, number>();
|
|
105
|
+
for (const doc of getAllDocs()) {
|
|
106
|
+
for (const tag of doc.frontmatter.tags) {
|
|
107
|
+
tagMap.set(tag, (tagMap.get(tag) ?? 0) + 1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return Array.from(tagMap.entries())
|
|
111
|
+
.map(([tag, count]) => ({ tag, count }))
|
|
112
|
+
.sort((a, b) => b.count - a.count);
|
|
113
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getAllDocs } from "./docs";
|
|
2
|
+
import type { SearchIndexEntry } from "./types";
|
|
3
|
+
|
|
4
|
+
export function buildSearchIndex(): SearchIndexEntry[] {
|
|
5
|
+
return getAllDocs().map((doc) => ({
|
|
6
|
+
slug: doc.slug,
|
|
7
|
+
category: doc.category,
|
|
8
|
+
title: doc.frontmatter.title,
|
|
9
|
+
tags: doc.frontmatter.tags,
|
|
10
|
+
excerpt: doc.excerpt,
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function searchIndex(
|
|
15
|
+
index: SearchIndexEntry[],
|
|
16
|
+
query: string,
|
|
17
|
+
): SearchIndexEntry[] {
|
|
18
|
+
const lower = query.toLowerCase();
|
|
19
|
+
return index.filter((entry) => {
|
|
20
|
+
return (
|
|
21
|
+
entry.title.toLowerCase().includes(lower) ||
|
|
22
|
+
entry.tags.some((t) => t.toLowerCase().includes(lower)) ||
|
|
23
|
+
entry.excerpt.toLowerCase().includes(lower) ||
|
|
24
|
+
entry.category.toLowerCase().includes(lower)
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface DocFrontmatter {
|
|
2
|
+
title: string;
|
|
3
|
+
category: string;
|
|
4
|
+
tags: string[];
|
|
5
|
+
created: string;
|
|
6
|
+
updated: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DocMeta {
|
|
10
|
+
slug: string;
|
|
11
|
+
category: string;
|
|
12
|
+
frontmatter: DocFrontmatter;
|
|
13
|
+
excerpt: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Doc extends DocMeta {
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CategoryInfo {
|
|
21
|
+
name: string;
|
|
22
|
+
docCount: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SearchIndexEntry {
|
|
26
|
+
slug: string;
|
|
27
|
+
category: string;
|
|
28
|
+
title: string;
|
|
29
|
+
tags: string[];
|
|
30
|
+
excerpt: string;
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": { "@/*": ["./src/*"] }
|
|
18
|
+
},
|
|
19
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
20
|
+
"exclude": ["node_modules"]
|
|
21
|
+
}
|