pmpt-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.
@@ -0,0 +1,233 @@
1
+ import { copyFileSync, existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs';
2
+ import { basename, join, relative } from 'path';
3
+ import { getHistoryDir, getPmptDir, loadConfig } from './config.js';
4
+ import { getGitInfo, isGitRepo } from './git.js';
5
+ import glob from 'fast-glob';
6
+ /**
7
+ * pmpt 폴더 전체를 스냅샷으로 저장
8
+ * .history/v{N}-{timestamp}/ 폴더에 모든 파일 복사
9
+ */
10
+ export function createFullSnapshot(projectPath) {
11
+ const historyDir = getHistoryDir(projectPath);
12
+ const pmptDir = getPmptDir(projectPath);
13
+ mkdirSync(historyDir, { recursive: true });
14
+ // 다음 버전 번호 찾기
15
+ const existing = getAllSnapshots(projectPath);
16
+ const version = existing.length + 1;
17
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
18
+ const snapshotName = `v${version}-${timestamp}`;
19
+ const snapshotDir = join(historyDir, snapshotName);
20
+ mkdirSync(snapshotDir, { recursive: true });
21
+ // pmpt 폴더의 모든 MD 파일 복사
22
+ const files = [];
23
+ if (existsSync(pmptDir)) {
24
+ const mdFiles = glob.sync('**/*.md', { cwd: pmptDir });
25
+ for (const file of mdFiles) {
26
+ const srcPath = join(pmptDir, file);
27
+ const destPath = join(snapshotDir, file);
28
+ // 하위 디렉토리가 있으면 생성
29
+ const destDir = join(snapshotDir, file.split('/').slice(0, -1).join('/'));
30
+ if (destDir !== snapshotDir) {
31
+ mkdirSync(destDir, { recursive: true });
32
+ }
33
+ copyFileSync(srcPath, destPath);
34
+ files.push(file);
35
+ }
36
+ }
37
+ // Git 정보 수집
38
+ const config = loadConfig(projectPath);
39
+ let gitData;
40
+ if (config?.trackGit && isGitRepo(projectPath)) {
41
+ const gitInfo = getGitInfo(projectPath, config.repo);
42
+ if (gitInfo) {
43
+ gitData = {
44
+ commit: gitInfo.commit,
45
+ commitFull: gitInfo.commitFull,
46
+ branch: gitInfo.branch,
47
+ dirty: gitInfo.dirty,
48
+ tag: gitInfo.tag,
49
+ };
50
+ }
51
+ }
52
+ // 메타데이터 저장
53
+ const metaPath = join(snapshotDir, '.meta.json');
54
+ writeFileSync(metaPath, JSON.stringify({
55
+ version,
56
+ timestamp,
57
+ files,
58
+ git: gitData,
59
+ }, null, 2), 'utf-8');
60
+ return {
61
+ version,
62
+ timestamp,
63
+ snapshotDir,
64
+ files,
65
+ git: gitData,
66
+ };
67
+ }
68
+ /**
69
+ * 단일 파일 스냅샷 (pmpt 폴더 내 특정 파일만)
70
+ * 기존 호환성을 위해 유지하되, 내부적으로 전체 스냅샷 사용
71
+ */
72
+ export function createSnapshot(projectPath, filePath) {
73
+ const historyDir = getHistoryDir(projectPath);
74
+ const pmptDir = getPmptDir(projectPath);
75
+ const relPath = relative(pmptDir, filePath);
76
+ // 파일이 pmpt 폴더 외부에 있는 경우
77
+ if (relPath.startsWith('..')) {
78
+ // 프로젝트 루트 기준 상대 경로 사용
79
+ const projectRelPath = relative(projectPath, filePath);
80
+ return createSingleFileSnapshot(projectPath, filePath, projectRelPath);
81
+ }
82
+ return createSingleFileSnapshot(projectPath, filePath, relPath);
83
+ }
84
+ function createSingleFileSnapshot(projectPath, filePath, relPath) {
85
+ const historyDir = getHistoryDir(projectPath);
86
+ const fileName = basename(filePath, '.md');
87
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
88
+ // 해당 파일의 기존 버전 수 확인
89
+ const existing = getFileHistory(projectPath, relPath);
90
+ const version = existing.length + 1;
91
+ // 버전 폴더 생성
92
+ const snapshotName = `v${version}-${timestamp}`;
93
+ const snapshotDir = join(historyDir, snapshotName);
94
+ mkdirSync(snapshotDir, { recursive: true });
95
+ // 파일 복사
96
+ const destPath = join(snapshotDir, basename(filePath));
97
+ copyFileSync(filePath, destPath);
98
+ // Git 정보 수집
99
+ const config = loadConfig(projectPath);
100
+ let gitData;
101
+ if (config?.trackGit && isGitRepo(projectPath)) {
102
+ const gitInfo = getGitInfo(projectPath, config.repo);
103
+ if (gitInfo) {
104
+ gitData = {
105
+ commit: gitInfo.commit,
106
+ commitFull: gitInfo.commitFull,
107
+ branch: gitInfo.branch,
108
+ dirty: gitInfo.dirty,
109
+ tag: gitInfo.tag,
110
+ };
111
+ }
112
+ }
113
+ // 메타데이터 저장
114
+ const metaPath = join(snapshotDir, '.meta.json');
115
+ writeFileSync(metaPath, JSON.stringify({
116
+ version,
117
+ timestamp,
118
+ filePath: relPath,
119
+ git: gitData,
120
+ }, null, 2), 'utf-8');
121
+ return {
122
+ version,
123
+ timestamp,
124
+ filePath: relPath,
125
+ historyPath: destPath,
126
+ git: gitData,
127
+ };
128
+ }
129
+ /**
130
+ * 모든 스냅샷 목록 조회
131
+ */
132
+ export function getAllSnapshots(projectPath) {
133
+ const historyDir = getHistoryDir(projectPath);
134
+ if (!existsSync(historyDir))
135
+ return [];
136
+ const entries = [];
137
+ const dirs = readdirSync(historyDir);
138
+ for (const dir of dirs) {
139
+ const match = dir.match(/^v(\d+)-(.+)$/);
140
+ if (!match)
141
+ continue;
142
+ const snapshotDir = join(historyDir, dir);
143
+ if (!statSync(snapshotDir).isDirectory())
144
+ continue;
145
+ const metaPath = join(snapshotDir, '.meta.json');
146
+ let meta = {};
147
+ if (existsSync(metaPath)) {
148
+ try {
149
+ meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
150
+ }
151
+ catch {
152
+ // 메타 파일 파싱 실패 시 기본값 사용
153
+ }
154
+ }
155
+ entries.push({
156
+ version: parseInt(match[1], 10),
157
+ timestamp: match[2].replace(/-/g, ':'),
158
+ snapshotDir,
159
+ files: meta.files || [],
160
+ git: meta.git,
161
+ });
162
+ }
163
+ return entries.sort((a, b) => a.version - b.version);
164
+ }
165
+ /**
166
+ * 특정 파일의 히스토리 조회 (하위 호환성)
167
+ */
168
+ export function getFileHistory(projectPath, relPath) {
169
+ const historyDir = getHistoryDir(projectPath);
170
+ if (!existsSync(historyDir))
171
+ return [];
172
+ const fileName = basename(relPath);
173
+ const entries = [];
174
+ const dirs = readdirSync(historyDir);
175
+ for (const dir of dirs) {
176
+ const match = dir.match(/^v(\d+)-(.+)$/);
177
+ if (!match)
178
+ continue;
179
+ const snapshotDir = join(historyDir, dir);
180
+ if (!statSync(snapshotDir).isDirectory())
181
+ continue;
182
+ const filePath = join(snapshotDir, fileName);
183
+ if (!existsSync(filePath))
184
+ continue;
185
+ const metaPath = join(snapshotDir, '.meta.json');
186
+ let gitData;
187
+ if (existsSync(metaPath)) {
188
+ try {
189
+ const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
190
+ gitData = meta.git;
191
+ }
192
+ catch {
193
+ // 메타 파일 파싱 실패 시 무시
194
+ }
195
+ }
196
+ entries.push({
197
+ version: parseInt(match[1], 10),
198
+ timestamp: match[2].replace(/-/g, ':'),
199
+ filePath: relPath,
200
+ historyPath: filePath,
201
+ git: gitData,
202
+ });
203
+ }
204
+ return entries.sort((a, b) => a.version - b.version);
205
+ }
206
+ /**
207
+ * 전체 히스토리 조회 (모든 스냅샷의 모든 파일)
208
+ */
209
+ export function getAllHistory(projectPath) {
210
+ const snapshots = getAllSnapshots(projectPath);
211
+ const entries = [];
212
+ for (const snapshot of snapshots) {
213
+ for (const file of snapshot.files) {
214
+ entries.push({
215
+ version: snapshot.version,
216
+ timestamp: snapshot.timestamp,
217
+ filePath: file,
218
+ historyPath: join(snapshot.snapshotDir, file),
219
+ git: snapshot.git,
220
+ });
221
+ }
222
+ }
223
+ return entries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
224
+ }
225
+ /**
226
+ * 추적 중인 파일 목록 (pmpt 폴더 기준)
227
+ */
228
+ export function getTrackedFiles(projectPath) {
229
+ const pmptDir = getPmptDir(projectPath);
230
+ if (!existsSync(pmptDir))
231
+ return [];
232
+ return glob.sync('**/*.md', { cwd: pmptDir });
233
+ }
@@ -0,0 +1,173 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { getConfigDir, getPmptDir } from './config.js';
4
+ import { createFullSnapshot } from './history.js';
5
+ const PLAN_FILE = 'plan-progress.json';
6
+ // 6 simple questions (answer in any language you prefer)
7
+ export const PLAN_QUESTIONS = [
8
+ {
9
+ key: 'projectName',
10
+ question: 'Project name?',
11
+ placeholder: 'my-awesome-app',
12
+ required: true,
13
+ },
14
+ {
15
+ key: 'problem',
16
+ question: 'What problem are you solving?',
17
+ placeholder: 'e.g., Developers spend too much time finding API docs',
18
+ multiline: true,
19
+ required: true,
20
+ },
21
+ {
22
+ key: 'solution',
23
+ question: 'How will you solve it?',
24
+ placeholder: 'e.g., AI analyzes code and auto-generates API docs',
25
+ multiline: true,
26
+ required: true,
27
+ },
28
+ {
29
+ key: 'targetUser',
30
+ question: 'Who is your target user?',
31
+ placeholder: 'e.g., Startup backend developers',
32
+ required: true,
33
+ },
34
+ {
35
+ key: 'coreFeatures',
36
+ question: 'Core features for MVP? (one per line)',
37
+ placeholder: 'e.g.:\nCode upload\nAI analysis\nDoc generation',
38
+ multiline: true,
39
+ required: true,
40
+ },
41
+ {
42
+ key: 'techStack',
43
+ question: 'Preferred tech stack? (optional)',
44
+ placeholder: 'e.g., React, Node.js, PostgreSQL',
45
+ multiline: false,
46
+ required: false,
47
+ },
48
+ ];
49
+ // Generate AI prompt (language-agnostic template)
50
+ export function generateAIPrompt(answers) {
51
+ const features = answers.coreFeatures
52
+ .split('\n')
53
+ .map((f) => f.trim())
54
+ .filter((f) => f)
55
+ .map((f) => `- ${f}`)
56
+ .join('\n');
57
+ const techSection = answers.techStack
58
+ ? `\n## Tech Stack Preferences\n${answers.techStack}\n`
59
+ : '';
60
+ return `# ${answers.projectName} — Product Development Request
61
+
62
+ ## Problem
63
+ ${answers.problem}
64
+
65
+ ## Proposed Solution
66
+ ${answers.solution}
67
+
68
+ ## Target User
69
+ ${answers.targetUser}
70
+
71
+ ## MVP Core Features
72
+ ${features}
73
+ ${techSection}
74
+ ---
75
+
76
+ Please help me build this product based on the requirements above.
77
+
78
+ 1. First, review the requirements and ask if anything is unclear.
79
+ 2. Propose a technical architecture.
80
+ 3. Outline the MVP implementation steps.
81
+ 4. Start coding from the first step.
82
+
83
+ I'll confirm progress at each step before moving to the next.
84
+
85
+ ## Documentation Rule
86
+
87
+ **Important:** Update this document (located at \`.promptwiki/pmpt/pmpt.md\`) at these moments:
88
+ - When architecture or tech decisions are finalized
89
+ - When a feature is implemented (mark as done)
90
+ - When a development phase is completed
91
+ - When requirements change or new decisions are made
92
+
93
+ Add a "## Progress" section below and keep it updated:
94
+ \`\`\`
95
+ ## Progress
96
+ - [x] Completed item
97
+ - [ ] Pending item
98
+ \`\`\`
99
+
100
+ This keeps a living record of our development journey.
101
+ `;
102
+ }
103
+ // Generate plan document
104
+ export function generatePlanDocument(answers) {
105
+ const features = answers.coreFeatures
106
+ .split('\n')
107
+ .map((f) => f.trim())
108
+ .filter((f) => f)
109
+ .map((f) => `- [ ] ${f}`)
110
+ .join('\n');
111
+ const techSection = answers.techStack
112
+ ? `\n## Tech Stack\n${answers.techStack}\n`
113
+ : '';
114
+ return `# ${answers.projectName}
115
+
116
+ ## Problem
117
+ ${answers.problem}
118
+
119
+ ## Solution
120
+ ${answers.solution}
121
+
122
+ ## Target User
123
+ ${answers.targetUser}
124
+
125
+ ## MVP Features
126
+ ${features}
127
+ ${techSection}
128
+ ---
129
+ *Generated by PromptWiki Plan*
130
+ `;
131
+ }
132
+ export function getPlanProgress(projectPath) {
133
+ const planPath = join(getConfigDir(projectPath), PLAN_FILE);
134
+ if (!existsSync(planPath))
135
+ return null;
136
+ try {
137
+ return JSON.parse(readFileSync(planPath, 'utf-8'));
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ export function savePlanProgress(projectPath, progress) {
144
+ const configDir = getConfigDir(projectPath);
145
+ mkdirSync(configDir, { recursive: true });
146
+ const planPath = join(configDir, PLAN_FILE);
147
+ progress.updatedAt = new Date().toISOString();
148
+ writeFileSync(planPath, JSON.stringify(progress, null, 2), 'utf-8');
149
+ }
150
+ export function initPlanProgress(projectPath) {
151
+ const progress = {
152
+ completed: false,
153
+ startedAt: new Date().toISOString(),
154
+ updatedAt: new Date().toISOString(),
155
+ };
156
+ savePlanProgress(projectPath, progress);
157
+ return progress;
158
+ }
159
+ export function savePlanDocuments(projectPath, answers) {
160
+ const pmptDir = getPmptDir(projectPath);
161
+ mkdirSync(pmptDir, { recursive: true });
162
+ // Save plan to pmpt folder
163
+ const planPath = join(pmptDir, 'plan.md');
164
+ const planContent = generatePlanDocument(answers);
165
+ writeFileSync(planPath, planContent, 'utf-8');
166
+ // Save AI prompt to pmpt folder
167
+ const promptPath = join(pmptDir, 'pmpt.md');
168
+ const promptContent = generateAIPrompt(answers);
169
+ writeFileSync(promptPath, promptContent, 'utf-8');
170
+ // Create initial snapshot
171
+ createFullSnapshot(projectPath);
172
+ return { planPath, promptPath };
173
+ }
@@ -0,0 +1,61 @@
1
+ import { z } from 'zod';
2
+ import matter from 'gray-matter';
3
+ import { readFileSync } from 'fs';
4
+ const frontmatterSchema = z.object({
5
+ title: z.string().min(5, '제목은 5자 이상이어야 합니다'),
6
+ purpose: z.enum(['guide', 'rule', 'template', 'example', 'reference']),
7
+ level: z.enum(['beginner', 'intermediate', 'advanced']),
8
+ lang: z.enum(['ko', 'en']),
9
+ persona: z.array(z.enum(['general', 'power-user', 'developer', 'organization'])).optional(),
10
+ status: z.enum(['draft', 'review', 'stable', 'recommended', 'deprecated']).optional(),
11
+ translationKey: z.string().optional(),
12
+ tags: z.array(z.string()).optional(),
13
+ created: z.string().optional(),
14
+ updated: z.string().optional(),
15
+ contributors: z.array(z.string()).optional(),
16
+ });
17
+ const FILE_PATH_RE = /^(ko|en)\/(guide|rule|template|example|reference)\/(beginner|intermediate|advanced)\/.+\.mdx?$/;
18
+ export function validate(filePath) {
19
+ const errors = [];
20
+ const warnings = [];
21
+ // 1. 파일 경로 규칙
22
+ const relative = filePath.replace(/^.*?(?=ko\/|en\/)/, '');
23
+ if (!FILE_PATH_RE.test(relative)) {
24
+ errors.push(`파일 경로가 규칙에 맞지 않습니다: {lang}/{purpose}/{level}/파일명.md`);
25
+ }
26
+ // 2. 파일 읽기
27
+ let raw;
28
+ try {
29
+ raw = readFileSync(filePath, 'utf-8');
30
+ }
31
+ catch {
32
+ errors.push('파일을 읽을 수 없습니다');
33
+ return { valid: false, errors, warnings };
34
+ }
35
+ // 3. frontmatter 파싱
36
+ const { data, content } = matter(raw);
37
+ const result = frontmatterSchema.safeParse(data);
38
+ if (!result.success) {
39
+ for (const issue of result.error.issues) {
40
+ const field = issue.path.join('.');
41
+ errors.push(`[${field}] ${issue.message}`);
42
+ }
43
+ }
44
+ // 4. 본문 길이
45
+ const bodyLength = content.trim().length;
46
+ if (bodyLength < 200) {
47
+ errors.push(`본문이 너무 짧습니다 (현재 ${bodyLength}자, 최소 200자)`);
48
+ }
49
+ // 5. 경고
50
+ if (!data.tags || data.tags.length === 0) {
51
+ warnings.push('tags를 추가하면 검색과 관련 문서 연결에 도움이 됩니다');
52
+ }
53
+ if (!data.persona) {
54
+ warnings.push('persona를 지정하면 대상 독자가 명확해집니다');
55
+ }
56
+ return {
57
+ valid: errors.length === 0,
58
+ errors,
59
+ warnings,
60
+ };
61
+ }
@@ -0,0 +1,37 @@
1
+ export function toSlug(title) {
2
+ return title
3
+ .toLowerCase()
4
+ .replace(/[^\w\s-]/g, '')
5
+ .trim()
6
+ .replace(/\s+/g, '-')
7
+ .replace(/-+/g, '-')
8
+ .slice(0, 60);
9
+ }
10
+ export function today() {
11
+ return new Date().toISOString().slice(0, 10);
12
+ }
13
+ export function generateFilePath(fm) {
14
+ const slug = toSlug(fm.title);
15
+ return `${fm.lang}/${fm.purpose}/${fm.level}/${slug}.md`;
16
+ }
17
+ export function generateContent(fm) {
18
+ const frontmatter = [
19
+ '---',
20
+ `title: "${fm.title}"`,
21
+ `purpose: ${fm.purpose}`,
22
+ `level: ${fm.level}`,
23
+ `lang: ${fm.lang}`,
24
+ fm.persona?.length ? `persona: [${fm.persona.map((p) => `"${p}"`).join(', ')}]` : null,
25
+ `status: draft`,
26
+ fm.tags?.length ? `tags: [${fm.tags.map((t) => `"${t}"`).join(', ')}]` : null,
27
+ `created: "${today()}"`,
28
+ `updated: "${today()}"`,
29
+ '---',
30
+ ]
31
+ .filter(Boolean)
32
+ .join('\n');
33
+ const body = fm.lang === 'ko'
34
+ ? `\n## 왜 중요한가\n\n<!-- 이 문서가 필요한 이유를 설명하세요 -->\n\n## 방법\n\n<!-- 단계별로 설명하세요 -->\n\n## 예시\n\n<!-- 실제 예시를 추가하세요 -->\n`
35
+ : `\n## Why it matters\n\n<!-- Explain why this document is needed -->\n\n## How to\n\n<!-- Explain step by step -->\n\n## Example\n\n<!-- Add a real example -->\n`;
36
+ return frontmatter + body;
37
+ }
@@ -0,0 +1,63 @@
1
+ import chokidar from 'chokidar';
2
+ import { loadConfig, getPmptDir } from './config.js';
3
+ import { createFullSnapshot } from './history.js';
4
+ import { readFileSync } from 'fs';
5
+ export function startWatching(projectPath, onSnapshot) {
6
+ const config = loadConfig(projectPath);
7
+ if (!config) {
8
+ throw new Error('Project not initialized. Run `pmpt init` first.');
9
+ }
10
+ const pmptDir = getPmptDir(projectPath);
11
+ // Watch all MD files in pmpt folder
12
+ const watcher = chokidar.watch('**/*.md', {
13
+ cwd: pmptDir,
14
+ ignoreInitial: true,
15
+ persistent: true,
16
+ awaitWriteFinish: {
17
+ stabilityThreshold: 500,
18
+ pollInterval: 100,
19
+ },
20
+ });
21
+ const fileContents = new Map();
22
+ let debounceTimer = null;
23
+ const saveSnapshot = () => {
24
+ const entry = createFullSnapshot(projectPath);
25
+ if (onSnapshot) {
26
+ onSnapshot(entry.version, entry.files, entry.git);
27
+ }
28
+ };
29
+ // Debounced snapshot save (1 second)
30
+ const debouncedSave = () => {
31
+ if (debounceTimer) {
32
+ clearTimeout(debounceTimer);
33
+ }
34
+ debounceTimer = setTimeout(saveSnapshot, 1000);
35
+ };
36
+ watcher.on('add', (path) => {
37
+ const fullPath = `${pmptDir}/${path}`;
38
+ try {
39
+ const content = readFileSync(fullPath, 'utf-8');
40
+ fileContents.set(path, content);
41
+ debouncedSave();
42
+ }
43
+ catch {
44
+ // Ignore file read errors
45
+ }
46
+ });
47
+ watcher.on('change', (path) => {
48
+ const fullPath = `${pmptDir}/${path}`;
49
+ try {
50
+ const newContent = readFileSync(fullPath, 'utf-8');
51
+ const oldContent = fileContents.get(path);
52
+ // Only snapshot if content actually changed
53
+ if (oldContent !== newContent) {
54
+ fileContents.set(path, newContent);
55
+ debouncedSave();
56
+ }
57
+ }
58
+ catch {
59
+ // Ignore file read errors
60
+ }
61
+ });
62
+ return watcher;
63
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "pmpt-cli",
3
+ "version": "1.0.0",
4
+ "description": "Record and share your AI-driven product development journey",
5
+ "type": "module",
6
+ "bin": {
7
+ "pmpt": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "keywords": [
13
+ "pmpt",
14
+ "cli",
15
+ "prompt",
16
+ "ai",
17
+ "spec",
18
+ "log",
19
+ "development"
20
+ ],
21
+ "homepage": "https://pmptwiki.com",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/promptwiki/cli.git"
25
+ },
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "dev": "tsc --watch",
33
+ "start": "node dist/index.js",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "dependencies": {
37
+ "@clack/prompts": "^0.7.0",
38
+ "@octokit/rest": "^21.0.0",
39
+ "chokidar": "^3.6.0",
40
+ "commander": "^12.0.0",
41
+ "fast-glob": "^3.3.0",
42
+ "gray-matter": "^4.0.3",
43
+ "zod": "^3.22.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^20.0.0",
47
+ "typescript": "^5.0.0"
48
+ }
49
+ }