edsger 0.2.2 → 0.2.3

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.
@@ -285,7 +285,7 @@ CRITICAL: Checklists are not optional suggestions - they are mandatory quality g
285
285
  **Important Rules**:
286
286
  1. **Git Workflow**:
287
287
  - ALWAYS pull latest changes from main before creating branch: git pull origin ${baseBranch} --rebase
288
- - ALWAYS create a new branch before making changes: git checkout -b feature/<feature-id>
288
+ - ALWAYS create a new branch before making changes: git checkout -b dev/<feature-id>
289
289
  - Use descriptive commit messages that explain what and why
290
290
  - Handle pre-commit checks properly: fix issues first, use workarounds only when necessary
291
291
 
@@ -323,7 +323,7 @@ You MUST end your response with a JSON object containing the implementation resu
323
323
  {
324
324
  "implementation_result": {
325
325
  "feature_id": "FEATURE_ID_PLACEHOLDER",
326
- "branch_name": "feature/FEATURE_ID",
326
+ "branch_name": "dev/FEATURE_ID",
327
327
  "files_modified": ["file1.ts", "file2.tsx"],
328
328
  "commit_hash": "abc123...",
329
329
  "summary": "Brief description of what was implemented",
@@ -409,7 +409,7 @@ Follow this systematic approach:
409
409
  \`git pull origin ${baseBranch} --rebase\`
410
410
 
411
411
  2. **CREATE BRANCH**: Create a new git branch from ${baseBranch}:
412
- \`git checkout -b feature/${featureId}\`
412
+ \`git checkout -b dev/${featureId}\`
413
413
 
414
414
  2. **ANALYZE CODEBASE**: Study the existing codebase structure to understand:
415
415
  - Technology stack and architecture
@@ -459,7 +459,7 @@ const parseImplementationResponse = (response, featureId) => {
459
459
  if (parsed.implementation_result) {
460
460
  const result = parsed.implementation_result;
461
461
  return {
462
- branchName: result.branch_name || `feature/${featureId}`,
462
+ branchName: result.branch_name || `dev/${featureId}`,
463
463
  filesModified: result.files_modified || [],
464
464
  commitHash: result.commit_hash || '',
465
465
  summary: result.summary || '',
@@ -30,7 +30,13 @@ export const analyzeFeatureWithMCP = async (options, config, checklistContext) =
30
30
  }
31
31
  const context = await fetchFeatureAnalysisContext(mcpServerUrl, mcpToken, featureId, verbose);
32
32
  const systemPrompt = createFeatureAnalysisSystemPrompt(config, mcpServerUrl, mcpToken, featureId);
33
- const contextInfo = formatFeatureAnalysisContext(context);
33
+ const { content: contextInfo, downloadedImages } = await formatFeatureAnalysisContext(context);
34
+ if (verbose && downloadedImages.length > 0) {
35
+ logInfo(`Downloaded ${downloadedImages.length} images for Claude Code:`);
36
+ downloadedImages.forEach((img) => {
37
+ logInfo(` - ${img.url} -> ${img.localPath}`);
38
+ });
39
+ }
34
40
  // Add checklist context to the analysis prompt
35
41
  let finalContextInfo = contextInfo;
36
42
  if (checklistContext && checklistContext.checklists.length > 0) {
@@ -108,28 +108,28 @@ export async function createPullRequest(config, feature) {
108
108
  if (currentBranch.startsWith('dev/')) {
109
109
  const featureId = currentBranch.replace('dev/', '');
110
110
  targetBranch = `feat/${featureId}`;
111
- // Create the feat/ branch from current dev/ branch
111
+ // Create the feat/ branch from base branch (main)
112
112
  if (verbose) {
113
- console.log(`📝 Creating target branch: ${targetBranch} from ${currentBranch}`);
113
+ console.log(`📝 Creating target branch: ${targetBranch} from ${baseBranch}`);
114
114
  }
115
115
  try {
116
116
  // Push current branch to remote first
117
117
  pushBranch(currentBranch, verbose);
118
- // Get the current branch SHA
119
- const { data: branchData } = await octokit.repos.getBranch({
118
+ // Get the base branch (main) SHA to create feat/ branch from
119
+ const { data: baseBranchData } = await octokit.repos.getBranch({
120
120
  owner,
121
121
  repo,
122
- branch: currentBranch,
122
+ branch: baseBranch,
123
123
  });
124
- // Create the feat/ branch
124
+ // Create the feat/ branch from main
125
125
  await octokit.git.createRef({
126
126
  owner,
127
127
  repo,
128
128
  ref: `refs/heads/${targetBranch}`,
129
- sha: branchData.commit.sha,
129
+ sha: baseBranchData.commit.sha,
130
130
  });
131
131
  if (verbose) {
132
- console.log(`✅ Created target branch: ${targetBranch}`);
132
+ console.log(`✅ Created target branch: ${targetBranch} from ${baseBranch}`);
133
133
  }
134
134
  }
135
135
  catch (error) {
@@ -29,7 +29,13 @@ export const generateTechnicalDesign = async (options, config, checklistContext)
29
29
  }
30
30
  const context = await fetchTechnicalDesignContext(mcpServerUrl, mcpToken, featureId, verbose);
31
31
  const systemPrompt = createTechnicalDesignSystemPrompt(config, mcpServerUrl, mcpToken, featureId);
32
- const contextInfo = formatTechnicalDesignContext(context);
32
+ const { content: contextInfo, downloadedImages } = await formatTechnicalDesignContext(context);
33
+ if (verbose && downloadedImages.length > 0) {
34
+ logInfo(`Downloaded ${downloadedImages.length} images for Claude Code:`);
35
+ downloadedImages.forEach((img) => {
36
+ logInfo(` - ${img.url} -> ${img.localPath}`);
37
+ });
38
+ }
33
39
  // Add checklist context to the design prompt
34
40
  let finalContextInfo = contextInfo;
35
41
  if (checklistContext && checklistContext.checklists.length > 0) {
@@ -1,4 +1,5 @@
1
1
  import type { UserStory, TestCase } from '../types/features.js';
2
+ import { type DownloadedImage } from '../utils/image-downloader.js';
2
3
  import { TechnicalDesignContext } from '../phases/technical-design/context-fetcher.js';
3
4
  import { FeatureAnalysisContext } from '../phases/feature-analysis/context-fetcher.js';
4
5
  import { CodeImplementationContext } from '../phases/code-implementation/context-fetcher.js';
@@ -14,16 +15,28 @@ export declare function formatTestCases(cases: TestCase[]): string;
14
15
  /**
15
16
  * Format technical design context for Claude Code prompts
16
17
  */
17
- export declare function formatTechnicalDesignContext(context: TechnicalDesignContext): string;
18
+ export declare function formatTechnicalDesignContext(context: TechnicalDesignContext): Promise<{
19
+ content: string;
20
+ downloadedImages: DownloadedImage[];
21
+ }>;
18
22
  /**
19
23
  * Format feature analysis context for Claude Code prompts
20
24
  */
21
- export declare function formatFeatureAnalysisContext(context: FeatureAnalysisContext): string;
25
+ export declare function formatFeatureAnalysisContext(context: FeatureAnalysisContext): Promise<{
26
+ content: string;
27
+ downloadedImages: DownloadedImage[];
28
+ }>;
22
29
  /**
23
30
  * Format code implementation context for Claude Code prompts
24
31
  */
25
- export declare function formatCodeImplementationContext(context: CodeImplementationContext): string;
32
+ export declare function formatCodeImplementationContext(context: CodeImplementationContext): Promise<{
33
+ content: string;
34
+ downloadedImages: DownloadedImage[];
35
+ }>;
26
36
  /**
27
37
  * Format functional testing context for Claude Code prompts
28
38
  */
29
- export declare function formatFunctionalTestingContext(context: FunctionalTestingContext): string;
39
+ export declare function formatFunctionalTestingContext(context: FunctionalTestingContext): Promise<{
40
+ content: string;
41
+ downloadedImages: DownloadedImage[];
42
+ }>;
@@ -1,3 +1,4 @@
1
+ import { downloadImagesForClaudeCode, } from '../utils/image-downloader.js';
1
2
  /**
2
3
  * Format user stories for display in prompts
3
4
  */
@@ -23,13 +24,19 @@ export function formatTestCases(cases) {
23
24
  /**
24
25
  * Format technical design context for Claude Code prompts
25
26
  */
26
- export function formatTechnicalDesignContext(context) {
27
- return `# Technical Design Context
27
+ export async function formatTechnicalDesignContext(context) {
28
+ const { processedMarkdown, downloadedImages } = await downloadImagesForClaudeCode(context.feature.description || 'No description provided', context.feature.id // Use feature ID for directory naming
29
+ );
30
+ const content = `# Technical Design Context
28
31
 
29
32
  ## Feature Information
30
33
  - **ID**: ${context.feature.id}
31
34
  - **Name**: ${context.feature.name}
32
- - **Description**: ${context.feature.description || 'No description provided'}
35
+ - **Description**:
36
+ ${processedMarkdown}
37
+
38
+ ${downloadedImages.length > 0 ? '**IMPORTANT**: The description contains images that have been downloaded locally. Please use the Read tool to view these images to fully understand the requirements.' : ''}
39
+
33
40
  - **Current Status**: ${context.feature.status}
34
41
 
35
42
  ## Product Information
@@ -49,17 +56,24 @@ ${context.feature.technical_design || 'No existing technical design found'}
49
56
  ---
50
57
 
51
58
  **Design Instructions**: Based on the above feature requirements, user stories, and test cases, create or enhance the comprehensive technical design. Focus on system architecture, component design, database schema, API specifications, security considerations, and implementation strategy.`;
59
+ return { content, downloadedImages };
52
60
  }
53
61
  /**
54
62
  * Format feature analysis context for Claude Code prompts
55
63
  */
56
- export function formatFeatureAnalysisContext(context) {
57
- return `# Feature Analysis Context
64
+ export async function formatFeatureAnalysisContext(context) {
65
+ const { processedMarkdown, downloadedImages } = await downloadImagesForClaudeCode(context.feature.description || 'No description provided', context.feature.id // Use feature ID for directory naming
66
+ );
67
+ const content = `# Feature Analysis Context
58
68
 
59
69
  ## Feature Information
60
70
  - **ID**: ${context.feature.id}
61
71
  - **Name**: ${context.feature.name}
62
- - **Description**: ${context.feature.description || 'No description provided'}
72
+ - **Description**:
73
+ ${processedMarkdown}
74
+
75
+ ${downloadedImages.length > 0 ? '**IMPORTANT**: The description contains images that have been downloaded locally. Please use the Read tool to view these images to fully understand the requirements.' : ''}
76
+
63
77
  - **Current Status**: ${context.feature.status}
64
78
 
65
79
  ## Product Information
@@ -79,17 +93,24 @@ ${context.feature.technical_design || 'No technical design available yet'}
79
93
  ---
80
94
 
81
95
  **Analysis Instructions**: Based on the above feature information and existing user stories/test cases, conduct comprehensive business analysis to identify gaps and create additional user stories and test cases that add business value.`;
96
+ return { content, downloadedImages };
82
97
  }
83
98
  /**
84
99
  * Format code implementation context for Claude Code prompts
85
100
  */
86
- export function formatCodeImplementationContext(context) {
87
- return `# Code Implementation Context
101
+ export async function formatCodeImplementationContext(context) {
102
+ const { processedMarkdown, downloadedImages } = await downloadImagesForClaudeCode(context.feature.description || 'No description provided', context.feature.id // Use feature ID for directory naming
103
+ );
104
+ const content = `# Code Implementation Context
88
105
 
89
106
  ## Feature Information
90
107
  - **ID**: ${context.feature.id}
91
108
  - **Name**: ${context.feature.name}
92
- - **Description**: ${context.feature.description || 'No description provided'}
109
+ - **Description**:
110
+ ${processedMarkdown}
111
+
112
+ ${downloadedImages.length > 0 ? '**IMPORTANT**: The description contains images that have been downloaded locally. Please use the Read tool to view these images to fully understand the requirements.' : ''}
113
+
93
114
  - **Current Status**: ${context.feature.status}
94
115
 
95
116
  ## Product Information
@@ -109,17 +130,24 @@ ${context.technical_design || 'No technical design available - implement based o
109
130
  ---
110
131
 
111
132
  **Implementation Instructions**: Based on the above requirements, user stories, test cases, and technical design, implement the complete feature functionality. Ensure all user stories are implemented and all test cases can pass.`;
133
+ return { content, downloadedImages };
112
134
  }
113
135
  /**
114
136
  * Format functional testing context for Claude Code prompts
115
137
  */
116
- export function formatFunctionalTestingContext(context) {
117
- return `# Functional Testing Context
138
+ export async function formatFunctionalTestingContext(context) {
139
+ const { processedMarkdown, downloadedImages } = await downloadImagesForClaudeCode(context.feature.description || 'No description provided', context.feature.id // Use feature ID for directory naming
140
+ );
141
+ const content = `# Functional Testing Context
118
142
 
119
143
  ## Feature Information
120
144
  - **ID**: ${context.feature.id}
121
145
  - **Name**: ${context.feature.name}
122
- - **Description**: ${context.feature.description || 'No description provided'}
146
+ - **Description**:
147
+ ${processedMarkdown}
148
+
149
+ ${downloadedImages.length > 0 ? '**IMPORTANT**: The description contains images that have been downloaded locally. Please use the Read tool to view these images to fully understand the requirements.' : ''}
150
+
123
151
  - **Current Status**: ${context.feature.status}
124
152
 
125
153
  ## Product Information
@@ -136,4 +164,5 @@ ${formatTestCases(context.test_cases)}
136
164
  ---
137
165
 
138
166
  **Testing Instructions**: The feature has been implemented. Execute comprehensive functional testing using headless Playwright to verify all user stories work correctly and all test cases pass. Test both positive and negative scenarios.`;
167
+ return { content, downloadedImages };
139
168
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Downloaded image info
3
+ */
4
+ export interface DownloadedImage {
5
+ url: string;
6
+ localPath: string;
7
+ alt: string;
8
+ }
9
+ /**
10
+ * Set current feature ID for image downloads
11
+ * Should be called at the start of pipeline execution
12
+ */
13
+ export declare function setCurrentFeatureId(featureId: string): void;
14
+ /**
15
+ * Reset image cache (useful for testing)
16
+ */
17
+ export declare function resetImageCache(): void;
18
+ /**
19
+ * Download images from URLs to temporary directory for Claude Code to read
20
+ * Uses cache to avoid downloading same image multiple times
21
+ *
22
+ * @param markdown - Markdown content with image URLs
23
+ * @param featureId - Feature ID for directory naming
24
+ */
25
+ export declare function downloadImagesForClaudeCode(markdown: string, featureId?: string): Promise<{
26
+ processedMarkdown: string;
27
+ downloadedImages: DownloadedImage[];
28
+ }>;
29
+ /**
30
+ * Clean markdown by removing width attributes
31
+ */
32
+ export declare function cleanMarkdownForClaudeCode(markdown: string): string;
@@ -0,0 +1,144 @@
1
+ import { createWriteStream } from 'fs';
2
+ import { mkdir, access } from 'fs/promises';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import { pipeline } from 'stream/promises';
6
+ import { createHash } from 'crypto';
7
+ import { logInfo, logError } from './logger.js';
8
+ // Cache for downloaded images in current session
9
+ // Key: image URL, Value: local path
10
+ const imageCache = new Map();
11
+ // Current feature ID for directory naming
12
+ let currentFeatureId = null;
13
+ /**
14
+ * Generate hash from URL to use as filename
15
+ * This ensures same URL always maps to same file
16
+ */
17
+ function hashUrl(url) {
18
+ return createHash('md5').update(url).digest('hex');
19
+ }
20
+ /**
21
+ * Get or create temp directory for current feature
22
+ */
23
+ function getTempDir(featureId) {
24
+ return join(tmpdir(), 'claude-code-images', `feature-${featureId}`);
25
+ }
26
+ /**
27
+ * Set current feature ID for image downloads
28
+ * Should be called at the start of pipeline execution
29
+ */
30
+ export function setCurrentFeatureId(featureId) {
31
+ if (currentFeatureId !== featureId) {
32
+ // Clear cache when switching to different feature
33
+ imageCache.clear();
34
+ currentFeatureId = featureId;
35
+ }
36
+ }
37
+ /**
38
+ * Reset image cache (useful for testing)
39
+ */
40
+ export function resetImageCache() {
41
+ imageCache.clear();
42
+ currentFeatureId = null;
43
+ }
44
+ /**
45
+ * Download images from URLs to temporary directory for Claude Code to read
46
+ * Uses cache to avoid downloading same image multiple times
47
+ *
48
+ * @param markdown - Markdown content with image URLs
49
+ * @param featureId - Feature ID for directory naming
50
+ */
51
+ export async function downloadImagesForClaudeCode(markdown, featureId) {
52
+ if (!markdown) {
53
+ return { processedMarkdown: markdown, downloadedImages: [] };
54
+ }
55
+ // Use provided featureId or fall back to currentFeatureId or timestamp
56
+ const effectiveFeatureId = featureId || currentFeatureId || Date.now().toString();
57
+ // Extract all image URLs from markdown
58
+ const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)(?:\{width=\d+\})?/g;
59
+ const matches = Array.from(markdown.matchAll(imageRegex));
60
+ if (matches.length === 0) {
61
+ return { processedMarkdown: markdown, downloadedImages: [] };
62
+ }
63
+ // Create temp directory for this feature
64
+ const tempDir = getTempDir(effectiveFeatureId);
65
+ await mkdir(tempDir, { recursive: true });
66
+ const downloadedImages = [];
67
+ let processedMarkdown = markdown;
68
+ // Download each image (or use cached version)
69
+ for (let i = 0; i < matches.length; i++) {
70
+ const match = matches[i];
71
+ const alt = match[1] || '';
72
+ const url = match[2];
73
+ try {
74
+ // Check if image is already cached
75
+ let localPath = imageCache.get(url);
76
+ if (localPath) {
77
+ // Verify cached file still exists
78
+ try {
79
+ await access(localPath);
80
+ logInfo(`Using cached image: ${url}`);
81
+ }
82
+ catch {
83
+ // Cached file no longer exists, remove from cache
84
+ imageCache.delete(url);
85
+ localPath = undefined;
86
+ }
87
+ }
88
+ if (!localPath) {
89
+ // Download image
90
+ logInfo(`Downloading image ${i + 1}/${matches.length}: ${url}`);
91
+ const response = await fetch(url);
92
+ if (!response.ok) {
93
+ logError(`Failed to download image: ${response.statusText}`);
94
+ continue;
95
+ }
96
+ // Determine file extension from URL or content-type
97
+ let extension = 'png';
98
+ const urlExt = url.split('.').pop()?.toLowerCase();
99
+ if (urlExt && ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(urlExt)) {
100
+ extension = urlExt;
101
+ }
102
+ else {
103
+ const contentType = response.headers.get('content-type');
104
+ if (contentType?.includes('jpeg'))
105
+ extension = 'jpg';
106
+ else if (contentType?.includes('png'))
107
+ extension = 'png';
108
+ else if (contentType?.includes('gif'))
109
+ extension = 'gif';
110
+ else if (contentType?.includes('webp'))
111
+ extension = 'webp';
112
+ }
113
+ // Use hash of URL as filename (ensures same URL = same file)
114
+ const urlHash = hashUrl(url);
115
+ const filename = `${urlHash}.${extension}`;
116
+ localPath = join(tempDir, filename);
117
+ // Use Node.js streams to save the file
118
+ const fileStream = createWriteStream(localPath);
119
+ if (response.body) {
120
+ await pipeline(response.body, fileStream);
121
+ }
122
+ // Cache the downloaded image
123
+ imageCache.set(url, localPath);
124
+ logInfo(`Downloaded to: ${localPath}`);
125
+ }
126
+ downloadedImages.push({ url, localPath, alt });
127
+ // Replace URL with local path in markdown
128
+ processedMarkdown = processedMarkdown.replace(match[0], `![${alt}](${localPath})`);
129
+ }
130
+ catch (error) {
131
+ logError(`Failed to download image ${url}: ${error}`);
132
+ }
133
+ }
134
+ return { processedMarkdown, downloadedImages };
135
+ }
136
+ /**
137
+ * Clean markdown by removing width attributes
138
+ */
139
+ export function cleanMarkdownForClaudeCode(markdown) {
140
+ if (!markdown)
141
+ return markdown;
142
+ // Remove {width=600} syntax to keep pure markdown
143
+ return markdown.replace(/!\[([^\]]*)\]\(([^)]+)\)\{width=\d+\}/g, '![$1]($2)');
144
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Convert HTML description to Markdown format
3
+ * Preserves image URLs so Claude Code can view them
4
+ */
5
+ export declare function htmlToMarkdown(html: string): string;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Convert HTML description to Markdown format
3
+ * Preserves image URLs so Claude Code can view them
4
+ */
5
+ export function htmlToMarkdown(html) {
6
+ if (!html)
7
+ return '';
8
+ return (html
9
+ // Convert images to markdown format with original URL
10
+ .replace(/<img[^>]+src="([^">]+)"[^>]*alt="([^">]*)"[^>]*>/gi, '![$2]($1)')
11
+ .replace(/<img[^>]+src="([^">]+)"[^>]*>/gi, '![]($1)')
12
+ // Convert links
13
+ .replace(/<a[^>]+href="([^">]+)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)')
14
+ // Convert headings
15
+ .replace(/<h1[^>]*>(.*?)<\/h1>/gi, '\n# $1\n')
16
+ .replace(/<h2[^>]*>(.*?)<\/h2>/gi, '\n## $1\n')
17
+ .replace(/<h3[^>]*>(.*?)<\/h3>/gi, '\n### $1\n')
18
+ .replace(/<h4[^>]*>(.*?)<\/h4>/gi, '\n#### $1\n')
19
+ .replace(/<h5[^>]*>(.*?)<\/h5>/gi, '\n##### $1\n')
20
+ .replace(/<h6[^>]*>(.*?)<\/h6>/gi, '\n###### $1\n')
21
+ // Convert lists
22
+ .replace(/<ul[^>]*>/gi, '\n')
23
+ .replace(/<\/ul>/gi, '\n')
24
+ .replace(/<ol[^>]*>/gi, '\n')
25
+ .replace(/<\/ol>/gi, '\n')
26
+ .replace(/<li[^>]*>(.*?)<\/li>/gi, '- $1\n')
27
+ // Convert paragraphs
28
+ .replace(/<\/p>/gi, '\n\n')
29
+ .replace(/<p[^>]*>/gi, '')
30
+ // Convert line breaks
31
+ .replace(/<br\s*\/?>/gi, '\n')
32
+ // Convert text formatting
33
+ .replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
34
+ .replace(/<b[^>]*>(.*?)<\/b>/gi, '**$1**')
35
+ .replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*')
36
+ .replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*')
37
+ .replace(/<u[^>]*>(.*?)<\/u>/gi, '__$1__')
38
+ // Convert code
39
+ .replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`')
40
+ .replace(/<pre[^>]*>(.*?)<\/pre>/gi, '\n```\n$1\n```\n')
41
+ // Convert blockquotes
42
+ .replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gi, '\n> $1\n')
43
+ // Remove remaining HTML tags
44
+ .replace(/<[^>]+>/g, '')
45
+ // Decode HTML entities
46
+ .replace(/&lt;/g, '<')
47
+ .replace(/&gt;/g, '>')
48
+ .replace(/&amp;/g, '&')
49
+ .replace(/&quot;/g, '"')
50
+ .replace(/&#39;/g, "'")
51
+ .replace(/&nbsp;/g, ' ')
52
+ // Clean up extra whitespace
53
+ .replace(/\n{3,}/g, '\n\n')
54
+ .trim());
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {