packmd 0.0.1 → 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.
Files changed (3) hide show
  1. package/README.md +148 -0
  2. package/dist/index.js +416 -1
  3. package/package.json +38 -5
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ ## packmd
2
+
3
+ **PackMD** is a command‑line tool that converts any GitHub repository, local directory, or web page into a clean, token‑efficient Markdown digest – ready to paste into ChatGPT, Claude, or any LLM.
4
+
5
+ ### Installation
6
+
7
+ **Global (recommended)**
8
+
9
+ ```bash
10
+ npm install -g packmd
11
+ ```
12
+
13
+ Now you can use `packmd` from anywhere:
14
+
15
+ ```bash
16
+ packmd --help
17
+ ```
18
+
19
+ **Run without installing (using npx)**
20
+
21
+ ```bash
22
+ npx packmd <target> [options]
23
+ ```
24
+
25
+ **From source (monorepo)**
26
+
27
+ ```bash
28
+ git clone https://github.com/thelastofinusa/packmd.git
29
+ cd packmd
30
+ bun install
31
+ cd packages/cli
32
+ bun run build
33
+ # run locally
34
+ node dist/index.js <target> [options]
35
+ ```
36
+
37
+ ### Quick Start
38
+
39
+ ```bash
40
+ # Generate a digest from a public GitHub repo
41
+ packmd https://github.com/vercel/next.js -o next.md
42
+
43
+ # Scrape a documentation website (requires Jina API key for higher limits)
44
+ packmd https://react.dev --jina-api-key YOUR_KEY --copy
45
+
46
+ # Digest the current directory (respects .gitignore)
47
+ packmd .
48
+
49
+ # Digest a local folder with custom options
50
+ packmd ~/projects/my-app -m 300 -s 50 --exclude "*.test.js"
51
+ ```
52
+
53
+ ### Usage
54
+
55
+ ```
56
+ packmd [target] [options]
57
+ ```
58
+
59
+ | Argument | Description |
60
+ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
61
+ | `target` | GitHub URL (e.g., `https://github.com/owner/repo`), owner/repo slug, local directory path, or any webpage URL. Defaults to current directory (`.`). |
62
+
63
+ **Options**
64
+
65
+ | Flag | Description |
66
+ | ----------------------------- | ------------------------------------------------------------------------------------------ |
67
+ | `-o, --output <path>` | Save the generated Markdown to a specific file (e.g., `digest.md`). |
68
+ | `-c, --copy` | Copy the output directly to your clipboard (no file saved). |
69
+ | `-t, --token <token>` | GitHub Personal Access Token for private repositories or higher rate limits. |
70
+ | `-m, --max-files <number>` | Maximum number of files to include (default: `200`). |
71
+ | `-s, --max-file-size <kb>` | Maximum file size in KB (files larger are skipped) (default: `100`). |
72
+ | `-i, --include <patterns...>` | Glob patterns to explicitly include (e.g., `"*.ts" "*.md"`). |
73
+ | `-e, --exclude <patterns...>` | Glob patterns to explicitly ignore (e.g., `"test/**" "*.log"`). |
74
+ | `--jina-api-key <key>` | Jina Reader API key – raises the free rate limit from 20 to 500 requests per minute. |
75
+ | `--no-gitignore` | Ignore `.gitignore` rules when scanning a local directory (by default they are respected). |
76
+ | `-v, --version` | Show the version number. |
77
+ | `-h, --help` | Show help. |
78
+
79
+ > **Note:** When using `--output`, if the file already exists, you will be prompted to overwrite or enter a new name.
80
+
81
+ ### Examples
82
+
83
+ **GitHub repository**
84
+
85
+ ```bash
86
+ # Basic
87
+ packmd facebook/react -o react.md
88
+
89
+ # With token (for private repos)
90
+ packmd my-org/private-repo -t ghp_xxxxx -o private.md
91
+
92
+ # Limit to 300 files, exclude everything in the "examples" folder
93
+ packmd vercel/next.js -m 300 -e "examples/**" -o next-limited.md
94
+ ```
95
+
96
+ **Web page (via Jina Reader)**
97
+
98
+ ```bash
99
+ # Simple scrape
100
+ packmd https://docs.nestjs.com --copy
101
+
102
+ # With Jina API key (higher rate limit)
103
+ packmd https://tailwindcss.com/docs --jina-api-key jina_xxxxx -o tailwind.md
104
+ ```
105
+
106
+ **Local directory**
107
+
108
+ ```bash
109
+ # Digest the current folder
110
+ packmd .
111
+
112
+ # Digest a specific path
113
+ packmd ~/code/my-project -o project.md
114
+
115
+ # Override .gitignore and include only TypeScript files
116
+ packmd ./src --no-gitignore -i "*.ts" "*.tsx"
117
+ ```
118
+
119
+ **Combine options**
120
+
121
+ ```bash
122
+ packmd https://github.com/expressjs/express -o express.md -m 100 -s 50 -e "test/**" "*.spec.js"
123
+ ```
124
+
125
+ ### How It Works
126
+
127
+ The CLI uses the same core engine (`@packmd/core`) as the web app:
128
+
129
+ 1. **Input detection** – determines if the target is a GitHub repo, a web page, or a local path.
130
+ 2. **Fetching** – for GitHub, uses the GitHub API; for web pages, uses Jina Reader; for local, uses Node.js `fs`.
131
+ 3. **Filtering** – applies ignore rules, globs, size limits, and binary detection.
132
+ 4. **Processing** – builds a directory tree and downloads file contents (in parallel for GitHub).
133
+ 5. **Markdown generation** – creates a header with metadata, an ASCII tree, and fenced code blocks for each file.
134
+
135
+ Progress is shown via a live spinner (`ora`). Output can be saved to a file or copied to your clipboard.
136
+
137
+ ### Notes
138
+
139
+ - **GitHub rate limits** – Without a token, you are limited to 60 requests per hour. Use `-t` to raise this to 5,000 per hour.
140
+ - **Jina rate limits** – Without an API key, you get 20 requests per minute. Provide `--jina-api-key` for 500 RPM.
141
+ - The CLI respects `.gitignore` by default. Use `--no-gitignore` to scan everything.
142
+ - **Binary files** (images, fonts, archives) are automatically skipped.
143
+
144
+ ---
145
+
146
+ ### License
147
+
148
+ MIT © [Holiday](https://github.com/thelastofinusa)
package/dist/index.js CHANGED
@@ -1,4 +1,419 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- console.log("Hello, World!");
4
+ import { Command } from "commander";
5
+
6
+ // src/actions/run.ts
7
+ import color2 from "picocolors";
8
+ import clipboard from "clipboardy";
9
+ import fs4 from "fs/promises";
10
+ import inquirer2 from "inquirer";
11
+ import ora from "ora";
12
+
13
+ // package.json
14
+ var name = "packmd";
15
+ var description = "Convert GitHub repositories and webpages into AI-ready Markdown digests";
16
+ var version = "1.0.0";
17
+
18
+ // src/utils/version-check.ts
19
+ import fs from "fs/promises";
20
+ import os from "os";
21
+ import path from "path";
22
+ import color from "picocolors";
23
+ var REGISTRY_URL = `https://registry.npmjs.org/${name}/latest`;
24
+ var CACHE_PATH = path.join(os.homedir(), `.${name}`, "version-check.json");
25
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
26
+ var FETCH_TIMEOUT_MS = 1500;
27
+ function compareVersions(a, b) {
28
+ const pa = a.split(".").map(Number);
29
+ const pb = b.split(".").map(Number);
30
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
31
+ const na = pa[i] || 0;
32
+ const nb = pb[i] || 0;
33
+ if (na > nb) return 1;
34
+ if (na < nb) return -1;
35
+ }
36
+ return 0;
37
+ }
38
+ async function readCache() {
39
+ try {
40
+ return JSON.parse(await fs.readFile(CACHE_PATH, "utf-8"));
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+ async function writeCache(data) {
46
+ try {
47
+ await fs.mkdir(path.dirname(CACHE_PATH), { recursive: true });
48
+ await fs.writeFile(CACHE_PATH, JSON.stringify(data), "utf-8");
49
+ } catch {
50
+ }
51
+ }
52
+ async function fetchLatestVersion() {
53
+ const controller = new AbortController();
54
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
55
+ try {
56
+ const res = await fetch(REGISTRY_URL, {
57
+ signal: controller.signal,
58
+ headers: { Accept: "application/vnd.npm.install-v1+json" }
59
+ // lightweight abbreviated response
60
+ });
61
+ if (!res.ok) return null;
62
+ const data = await res.json();
63
+ return typeof data.version === "string" ? data.version : null;
64
+ } catch {
65
+ return null;
66
+ } finally {
67
+ clearTimeout(timeout);
68
+ }
69
+ }
70
+ async function checkForUpdate(currentVersion) {
71
+ const cache = await readCache();
72
+ const isFresh = cache && Date.now() - cache.lastChecked < CACHE_TTL_MS;
73
+ const latestVersion = isFresh ? cache.latestVersion : await fetchLatestVersion();
74
+ if (!latestVersion) return null;
75
+ if (!isFresh) await writeCache({ lastChecked: Date.now(), latestVersion });
76
+ return compareVersions(latestVersion, currentVersion) > 0 ? latestVersion : null;
77
+ }
78
+ function printUpdateNotice(currentVersion, latestVersion) {
79
+ console.log();
80
+ console.log(
81
+ color.yellow(` \u2191 Update available: `) + color.dim(currentVersion) + color.yellow(" \u2192 ") + color.green(latestVersion)
82
+ );
83
+ console.log(
84
+ color.dim(` Run `) + color.cyan(`npm install -g ${name}@latest`) + color.dim(` to update.`)
85
+ );
86
+ console.log();
87
+ }
88
+
89
+ // src/handlers/github.ts
90
+ import { fetchGithubRepo } from "@packmd/core";
91
+ async function handleGitHub(target, options, spinner) {
92
+ const result = await fetchGithubRepo(target, {
93
+ token: options.token || process.env.GITHUB_TOKEN,
94
+ maxFiles: Number(options.maxFiles),
95
+ maxFileSizeKB: Number(options.maxFileSize),
96
+ includeGlobs: options.include || [],
97
+ excludeGlobs: options.exclude || [],
98
+ onProgress: (msg) => {
99
+ spinner.text = msg;
100
+ }
101
+ });
102
+ return result.markdown;
103
+ }
104
+
105
+ // src/handlers/local.ts
106
+ import path4 from "path";
107
+ import { DEFAULT_IGNORES, buildDigestHeader } from "@packmd/core";
108
+
109
+ // src/utils/fs.ts
110
+ import fs2 from "fs/promises";
111
+ import path2 from "path";
112
+ import { minimatch } from "minimatch";
113
+ async function walkLocalDir(dir, base, excludeGlobs, includeGlobs, maxFileSizeKB, maxFiles, gitignore = null, collected = []) {
114
+ if (collected.length >= maxFiles) return collected;
115
+ const entries = await fs2.readdir(dir);
116
+ for (const name2 of entries) {
117
+ if (collected.length >= maxFiles) break;
118
+ const fullPath = path2.join(dir, name2);
119
+ const relativePath = path2.relative(base, fullPath).replace(/\\/g, "/");
120
+ const isExcluded = excludeGlobs.some(
121
+ (g) => minimatch(relativePath, g, { dot: true })
122
+ );
123
+ if (isExcluded) continue;
124
+ const stats = await fs2.stat(fullPath);
125
+ if (gitignore?.ignores(
126
+ stats.isDirectory() ? `${relativePath}/` : relativePath
127
+ )) {
128
+ continue;
129
+ }
130
+ if (stats.isDirectory()) {
131
+ await walkLocalDir(
132
+ fullPath,
133
+ base,
134
+ excludeGlobs,
135
+ includeGlobs,
136
+ maxFileSizeKB,
137
+ maxFiles,
138
+ gitignore,
139
+ collected
140
+ );
141
+ } else if (stats.isFile()) {
142
+ if (includeGlobs.length > 0) {
143
+ const isIncluded = includeGlobs.some(
144
+ (g) => minimatch(relativePath, g, { dot: true })
145
+ );
146
+ if (!isIncluded) continue;
147
+ }
148
+ const sizeKB = stats.size / 1024;
149
+ if (sizeKB <= maxFileSizeKB) {
150
+ const content = await fs2.readFile(fullPath, "utf-8");
151
+ collected.push({ path: relativePath, content });
152
+ }
153
+ }
154
+ }
155
+ return collected;
156
+ }
157
+
158
+ // src/utils/gitignore.ts
159
+ import fs3 from "fs/promises";
160
+ import path3 from "path";
161
+ import ignore from "ignore";
162
+ async function loadGitignore(startDir) {
163
+ const ig = ignore();
164
+ let found = false;
165
+ let dir = startDir;
166
+ while (true) {
167
+ try {
168
+ const content = await fs3.readFile(path3.join(dir, ".gitignore"), "utf-8");
169
+ ig.add(content);
170
+ found = true;
171
+ } catch {
172
+ }
173
+ try {
174
+ await fs3.access(path3.join(dir, ".git"));
175
+ break;
176
+ } catch {
177
+ }
178
+ const parent = path3.dirname(dir);
179
+ if (parent === dir) break;
180
+ dir = parent;
181
+ }
182
+ return found ? ig : null;
183
+ }
184
+
185
+ // src/handlers/local.ts
186
+ async function handleLocalDir(target, options, spinner) {
187
+ const absolutePath = path4.resolve(process.cwd(), target);
188
+ const excludeGlobs = options.exclude || DEFAULT_IGNORES;
189
+ const includeGlobs = options.include || [];
190
+ const maxFiles = Number(options.maxFiles);
191
+ const maxFileSizeKB = Number(options.maxFileSize);
192
+ const gitignore = options.gitignore === false ? null : await loadGitignore(absolutePath);
193
+ const files = await walkLocalDir(
194
+ absolutePath,
195
+ absolutePath,
196
+ excludeGlobs,
197
+ includeGlobs,
198
+ maxFileSizeKB,
199
+ maxFiles,
200
+ gitignore
201
+ );
202
+ spinner.text = `Processing ${files.length} local files..`;
203
+ const totalChars = files.reduce((sum, f) => sum + f.content.length, 0);
204
+ const estTokens = Math.round(totalChars / 4);
205
+ const header = buildDigestHeader({
206
+ title: `Local Digest \u2014 \`${path4.basename(absolutePath)}\``,
207
+ meta: {
208
+ Path: `\`${absolutePath}\``,
209
+ Date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
210
+ Files: files.length,
211
+ "Est. tokens": `~${estTokens.toLocaleString()}`
212
+ }
213
+ });
214
+ const markdownParts = [header];
215
+ for (const file of files) {
216
+ markdownParts.push(`## File: ${file.path}
217
+ \`\`\`
218
+ ${file.content}
219
+ \`\`\``);
220
+ }
221
+ return markdownParts.join("\n\n");
222
+ }
223
+
224
+ // src/handlers/webpage.ts
225
+ import { buildDigestHeader as buildDigestHeader2, scrapeWebPage } from "@packmd/core";
226
+ async function handleWebpage(target, options) {
227
+ const scraped = await scrapeWebPage(target, {
228
+ jinaApiKey: options.jinaApiKey || process.env.JINA_API_KEY
229
+ });
230
+ const estTokens = Math.round((scraped.content?.length || 0) / 4);
231
+ const header = buildDigestHeader2({
232
+ icon: "\u{1F310}",
233
+ title: scraped.title || target,
234
+ meta: {
235
+ Source: `\`${target}\``,
236
+ Date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
237
+ "Est. tokens": `~${estTokens.toLocaleString()}`
238
+ }
239
+ });
240
+ return `${header}
241
+
242
+ ${scraped.content || ""}`;
243
+ }
244
+
245
+ // src/prompts/github.ts
246
+ import inquirer from "inquirer";
247
+ async function promptGithubOptions(currentOptions) {
248
+ const { customize } = await inquirer.prompt([
249
+ {
250
+ type: "confirm",
251
+ name: "customize",
252
+ message: "GitHub URL detected. Would you like to configure options?",
253
+ default: false
254
+ }
255
+ ]);
256
+ if (!customize) return currentOptions;
257
+ const answers = await inquirer.prompt([
258
+ {
259
+ type: "input",
260
+ name: "maxFiles",
261
+ message: "Maximum number of files to include:",
262
+ default: currentOptions.maxFiles || "200",
263
+ validate: (val) => !isNaN(Number(val)) || "Please enter a valid number"
264
+ },
265
+ {
266
+ type: "input",
267
+ name: "maxFileSize",
268
+ message: "Maximum file size threshold (in KB):",
269
+ default: currentOptions.maxFileSize || "100",
270
+ validate: (val) => !isNaN(Number(val)) || "Please enter a valid number"
271
+ },
272
+ {
273
+ type: "password",
274
+ name: "token",
275
+ message: "GitHub Token (Optional - for private or larger repos):",
276
+ default: currentOptions.token || ""
277
+ }
278
+ ]);
279
+ return { ...currentOptions, ...answers };
280
+ }
281
+
282
+ // src/actions/run.ts
283
+ async function runAction(target, options) {
284
+ console.log(color2.cyan(`${name} \u2014 AI Markdown Packager`));
285
+ const updateCheck = checkForUpdate(version).catch(() => null);
286
+ let finalOptions = { ...options };
287
+ try {
288
+ let digest = "";
289
+ const spinner = ora();
290
+ if (target.startsWith("http://") || target.startsWith("https://")) {
291
+ const targetUrl = new URL(target);
292
+ const isGitHub = targetUrl.hostname.includes("github.com");
293
+ if (isGitHub) {
294
+ finalOptions = await promptGithubOptions(finalOptions);
295
+ spinner.start("Fetching repository. Please wait..");
296
+ digest = await handleGitHub(target, finalOptions, spinner);
297
+ } else {
298
+ spinner.start("Scraping webpage. Please wait..");
299
+ digest = await handleWebpage(target, finalOptions);
300
+ }
301
+ } else {
302
+ spinner.start("Scanning local directory. Please wait..");
303
+ digest = await handleLocalDir(target, finalOptions, spinner);
304
+ }
305
+ let outputPath = finalOptions.copy ? null : finalOptions.output || "pack.md";
306
+ if (outputPath) {
307
+ let finalPath = outputPath;
308
+ while (true) {
309
+ let fileExists = false;
310
+ try {
311
+ await fs4.access(finalPath);
312
+ fileExists = true;
313
+ } catch {
314
+ fileExists = false;
315
+ }
316
+ if (fileExists) {
317
+ const { overwrite } = await inquirer2.prompt([
318
+ {
319
+ type: "confirm",
320
+ name: "overwrite",
321
+ message: `File ${color2.cyan(finalPath)} already exists. Overwrite?`,
322
+ default: false
323
+ }
324
+ ]);
325
+ if (overwrite) {
326
+ break;
327
+ } else {
328
+ const { newFileName } = await inquirer2.prompt([
329
+ {
330
+ type: "input",
331
+ name: "newFileName",
332
+ message: "Please enter a new file name:",
333
+ default: "pack-new.md",
334
+ validate: (val) => val.trim().length > 0 || "File name cannot be empty."
335
+ }
336
+ ]);
337
+ finalPath = newFileName;
338
+ }
339
+ } else {
340
+ break;
341
+ }
342
+ }
343
+ await fs4.writeFile(finalPath, digest, { encoding: "utf-8", flag: "w" });
344
+ spinner.succeed("Markdown generated successfully!");
345
+ }
346
+ if (finalOptions.copy) {
347
+ await clipboard.write(digest);
348
+ spinner.succeed("Markdown compiled successfully.");
349
+ }
350
+ const latestVersion = await updateCheck;
351
+ if (latestVersion) printUpdateNotice(version, latestVersion);
352
+ } catch (error) {
353
+ console.error(
354
+ color2.red(`
355
+ \u2716 ${error.message || "An unexpected error occurred"}`)
356
+ );
357
+ process.exit(1);
358
+ }
359
+ }
360
+
361
+ // src/utils/program.ts
362
+ var cliOptions = [
363
+ {
364
+ flags: "-o, --output <path>",
365
+ description: "Specify a custom file path to save the generated Markdown digest"
366
+ },
367
+ {
368
+ flags: "-t, --token <token>",
369
+ description: "Provide a GitHub Personal Access Token for private repositories or extended rate limits"
370
+ },
371
+ {
372
+ flags: "-m, --max-files <number>",
373
+ description: "Limit the total number of files to include in the final digest",
374
+ defaultValue: "200"
375
+ },
376
+ {
377
+ flags: "-s, --max-file-size <number>",
378
+ description: "Set the maximum allowed size (in KB) for individual files",
379
+ defaultValue: "100"
380
+ },
381
+ {
382
+ flags: "-i, --include <patterns...>",
383
+ description: "Specify glob patterns to explicitly include certain files or directories"
384
+ },
385
+ {
386
+ flags: "-e, --exclude <patterns...>",
387
+ description: "Specify glob patterns to explicitly ignore certain files or directories"
388
+ },
389
+ {
390
+ flags: "-c, --copy",
391
+ description: "Automatically copy the generated Markdown digest to your clipboard",
392
+ defaultValue: false
393
+ },
394
+ {
395
+ flags: "--jina-api-key <key>",
396
+ description: "Provide a Jina API key for enhanced webpage scraping and parsing"
397
+ },
398
+ {
399
+ flags: "--no-gitignore",
400
+ description: "Don't respect .gitignore rules when scanning a local directory"
401
+ }
402
+ ];
403
+
404
+ // src/index.ts
405
+ var program = new Command();
406
+ program.name(name).description(description).version(version, "-v, --version", "Output the version number").argument(
407
+ "[target]",
408
+ "GitHub URL, Webpage URL, or local directory path (defaults to current directory)",
409
+ "."
410
+ );
411
+ cliOptions.forEach(({ flags, description: description2, defaultValue }) => {
412
+ if (defaultValue !== void 0) {
413
+ program.option(flags, description2, defaultValue);
414
+ } else {
415
+ program.option(flags, description2);
416
+ }
417
+ });
418
+ program.action(runAction);
419
+ program.parse();
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "packmd",
3
- "version": "0.0.1",
3
+ "description": "Convert GitHub repositories and webpages into AI-ready Markdown digests",
4
+ "version": "1.0.0",
4
5
  "license": "MIT",
5
6
  "type": "module",
6
7
  "bin": {
@@ -13,13 +14,15 @@
13
14
  ],
14
15
  "scripts": {
15
16
  "build": "tsup",
16
- "dev": "tsx src/index.ts",
17
- "start": "node dist/index.js",
17
+ "start": "tsx src/index.js",
18
+ "start:node": "node dist/index.js",
19
+ "clean": "rm -rf dist node_modules .turbo",
18
20
  "prepublishOnly": "bun run build",
19
21
  "pushout": "npm publish",
20
22
  "pushout-inc": "npm version patch --no-git-tag-version && bun run pushout"
21
23
  },
22
24
  "devDependencies": {
25
+ "@types/inquirer": "^9.0.10",
23
26
  "@types/node": "^26.1.1",
24
27
  "tsup": "^8.5.1",
25
28
  "tsx": "^4.23.1",
@@ -27,7 +30,37 @@
27
30
  },
28
31
  "dependencies": {
29
32
  "chalk": "^5.6.2",
33
+ "clipboardy": "^5.3.2",
30
34
  "commander": "^15.0.0",
31
- "ora": "^9.4.1"
32
- }
35
+ "ignore": "^7.0.6",
36
+ "inquirer": "^14.0.2",
37
+ "minimatch": "^10.2.5",
38
+ "ora": "^9.4.1",
39
+ "@packmd/core": "^1.0.0",
40
+ "picocolors": "^1.1.1"
41
+ },
42
+ "keywords": [
43
+ "packmd",
44
+ "markdown",
45
+ "github",
46
+ "llm",
47
+ "codebase",
48
+ "digest",
49
+ "cli",
50
+ "generator",
51
+ "scraper",
52
+ "developer-tools",
53
+ "chatgpt",
54
+ "claude"
55
+ ],
56
+ "author": "Holiday (https://github.com/thelastofinusa)",
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/thelastofinusa/packmd.git",
60
+ "directory": "packages/cli"
61
+ },
62
+ "bugs": {
63
+ "url": "https://github.com/thelastofinusa/packmd/issues"
64
+ },
65
+ "homepage": "https://packmd.vercel.app/docs"
33
66
  }