mkctx 3.0.0 → 4.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 +20 -14
  2. package/bin/mkctx.js +312 -164
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,6 +23,7 @@
23
23
  - šŸš€ **Multi-platform** - Works on Windows, macOS, and Linux
24
24
  - šŸ“ **Smart Ignoring** - Respects custom ignore patterns and common system files
25
25
  - āš™ļø **Configurable** - Customize source directories, output locations, and comments
26
+ - āœļø **Custom Naming** - Specify custom filenames for your outputs or use the default 'context'
26
27
  - šŸŽÆ **AI-Friendly** - Outputs code in markdown format ideal for AI prompts
27
28
  - šŸŽØ **Syntax Highlighting** - Proper language detection for code blocks
28
29
  - šŸ”„ **Dynamic Mode** - Interactive path selection when needed
@@ -49,7 +50,7 @@ mkctx
49
50
  This opens an interactive menu where you can:
50
51
  1. Generate context from config file or dynamically
51
52
  2. View context statistics
52
- 3. Save the context to a file
53
+ 3. Choose output formats and save with a custom name
53
54
 
54
55
  ### Create Configuration File
55
56
 
@@ -86,12 +87,18 @@ After generating context:
86
87
  Size: 156.23 KB
87
88
  Est. tokens: ~39,058
88
89
 
89
- ? What would you like to do with this context?
90
- āÆ šŸ’¾ Save context to file
91
- šŸ”™ Back to main menu
92
- āŒ Exit
90
+ ? Select output format:
91
+ āÆ šŸ“¦ All formats (MD, JSON, TOON, XML)
92
+ šŸ“ Markdown (.md)
93
+ šŸ”§ JSON (.json) - Simple array
94
+ šŸŽ’ TOON (.toon) - Token-optimized
95
+ šŸ“„ XML (.xml)
96
+
97
+ ? Enter a name for the output files: (context)
93
98
  ```
94
99
 
100
+ > **Note:** Simply press Enter to use the default name `context`.
101
+
95
102
  ## āš™ļø Configuration
96
103
 
97
104
  ### Project Configuration (`mkctx.config.json`)
@@ -144,7 +151,7 @@ These are always ignored automatically:
144
151
 
145
152
  ## šŸ“„ Output Format
146
153
 
147
- The generated `context.md` file contains your project code:
154
+ The generated file contains your project code. When saving, you can specify a filename (e.g., `my_project_docs.md`) or use the default.
148
155
 
149
156
  ````markdown
150
157
  /* Project Context */
@@ -206,7 +213,7 @@ mkctx automatically detects and applies proper syntax highlighting for:
206
213
  - **šŸ“š Code Understanding** - Share project overview for quick understanding
207
214
  - **šŸ‘„ Code Reviews** - Share project overview with reviewers
208
215
  - **šŸŽ“ Onboarding** - Help new developers understand the project
209
- - **šŸ“ Documentation** - Generate a snapshot of your codebase
216
+ - **šŸ“ Documentation** - Generate a snapshot of your codebase with custom naming
210
217
 
211
218
  ## šŸ–„ļø Platform Support
212
219
 
@@ -236,6 +243,11 @@ Or fix npm permissions: https://docs.npmjs.com/resolving-eacces-permissions-erro
236
243
 
237
244
  ## šŸ“‹ Changelog
238
245
 
246
+ ### v4.0.0
247
+
248
+ - āœļø Added interactive filename selection when saving (defaults to "context")
249
+ - šŸŽØ Improved UI/UX for file saving workflow
250
+
239
251
  ### v3.0.0
240
252
 
241
253
  - šŸŽÆ Simplified to focus on context generation
@@ -258,10 +270,4 @@ Contributions are welcome! Please feel free to submit pull requests or open issu
258
270
 
259
271
  ## šŸ“„ License
260
272
 
261
- MIT License - see [LICENSE](LICENSE) file for details.
262
-
263
- ---
264
-
265
- <p align="center">
266
- Made with ā¤ļø for developers who love AI-assisted coding
267
- </p>
273
+ MIT License - see [LICENSE](LICENSE) file for details.
package/bin/mkctx.js CHANGED
@@ -25,7 +25,7 @@ const CONFIG_FILE = 'mkctx.config.json';
25
25
 
26
26
  const DEFAULT_PROJECT_CONFIG = {
27
27
  src: ".",
28
- ignore: "mkctx.config.json, pnpm-lock.yaml, **/.titan/, mkctx/, node_modules/, .git/, dist/, build/, target/, .next/, out/, .cache, package-lock.json, README.md, *.log, temp/, tmp/, coverage/, .nyc_output, .env, .env.local, .env.development.local, .env.test.local, .env.production.local, npm-debug.log*, yarn-debug.log*, yarn-error.log*, .npm, .yarn-integrity, .parcel-cache, .vuepress/dist, .svelte-kit, **/*.rs.bk, .idea/, .vscode/, .DS_Store, Thumbs.db, *.swp, *.swo, .~lock.*, Cargo.lock, .cargo/registry/, .cargo/git/, .rustup/, *.pdb, *.dSYM/, *.so, *.dll, *.dylib, *.exe, *.lib, *.a, *.o, *.rlib, *.d, *.tmp, *.bak, *.orig, *.rej, *.pyc, *.pyo, *.class, *.jar, *.war, *.ear, *.zip, *.tar.gz, *.rar, *.7z, *.iso, *.img, *.dmg, *.pdf, *.doc, *.docx, *.xls, *.xlsx, *.ppt, *.pptx",
28
+ ignore: "mkctx.config.json, pnpm-lock.yaml, **/.titan/, mkctx/, node_modules/, .git/, dist/, build/, target/, .next/, out/, .cache, package-lock.json, *.log, temp/, tmp/, coverage/, .nyc_output, .env, .env.local, .env.development.local, .env.test.local, .env.production.local, npm-debug.log*, yarn-debug.log*, yarn-error.log*, .npm, .yarn-integrity, .parcel-cache, .vuepress/dist, .svelte-kit, **/*.rs.bk, .idea/, .vscode/, .DS_Store, Thumbs.db, *.swp, *.swo, .~lock.*, Cargo.lock, .cargo/registry/, .cargo/git/, .rustup/, *.pdb, *.dSYM/, *.so, *.dll, *.dylib, *.exe, *.lib, *.a, *.o, *.rlib, *.d, *.tmp, *.bak, *.orig, *.rej, *.pyc, *.pyo, *.class, *.jar, *.war, *.ear, *.zip, *.tar.gz, *.rar, *.7z, *.iso, *.img, *.dmg, *.pdf, *.doc, *.docx, *.xls, *.xlsx, *.ppt, *.pptx",
29
29
  output: "./mkctx",
30
30
  first_comment: "/* Project Context */",
31
31
  last_comment: "/* End of Context */"
@@ -73,17 +73,11 @@ const KNOWN_FILES = new Set([
73
73
  'procfile', 'vagrantfile', 'jenkinsfile',
74
74
  '.gitignore', '.gitattributes', '.editorconfig',
75
75
  '.eslintrc', '.prettierrc', '.babelrc',
76
- '.env', '.env.example', '.env.local'
76
+ '.env', '.env.example', '.env.local',
77
+ 'readme.md', 'readme.txt', 'readme',
78
+ 'license', 'license.md', 'license.txt'
77
79
  ]);
78
80
 
79
- // ============================================
80
- // GLOBAL STATE
81
- // ============================================
82
-
83
- let generatedContext = null;
84
- let contextFiles = [];
85
- let contextStats = {};
86
-
87
81
  // ============================================
88
82
  // CONFIGURATION MANAGEMENT
89
83
  // ============================================
@@ -107,7 +101,7 @@ function hasProjectConfig() {
107
101
 
108
102
  function createProjectConfig() {
109
103
  loadDependencies();
110
-
104
+
111
105
  if (!fs.existsSync('mkctx')) {
112
106
  fs.mkdirSync('mkctx', { recursive: true });
113
107
  }
@@ -151,6 +145,11 @@ function estimateTokens(text) {
151
145
  return Math.ceil(text.length / 4);
152
146
  }
153
147
 
148
+ // Normalize path to always use forward slashes
149
+ function normalizePath(filePath) {
150
+ return filePath.replace(/\\/g, '/');
151
+ }
152
+
154
153
  // ============================================
155
154
  // FILE OPERATIONS
156
155
  // ============================================
@@ -194,6 +193,10 @@ function matchWildcard(pattern, filename) {
194
193
  }
195
194
 
196
195
  function shouldIgnore(fullPath, name, relativePath, patterns) {
196
+ // Normalize paths for comparison
197
+ const normalizedFull = normalizePath(fullPath);
198
+ const normalizedRelative = normalizePath(relativePath);
199
+
197
200
  const systemIgnores = [
198
201
  '.git', '.DS_Store', 'Thumbs.db', 'node_modules',
199
202
  '.svn', '.hg', '__pycache__', '.pytest_cache',
@@ -201,8 +204,9 @@ function shouldIgnore(fullPath, name, relativePath, patterns) {
201
204
  ];
202
205
 
203
206
  for (const ignore of systemIgnores) {
204
- if (fullPath.includes(path.sep + ignore + path.sep) ||
205
- fullPath.includes(ignore + path.sep) ||
207
+ if (normalizedFull.includes('/' + ignore + '/') ||
208
+ normalizedFull.includes('/' + ignore) ||
209
+ normalizedFull.endsWith('/' + ignore) ||
206
210
  name === ignore) {
207
211
  return true;
208
212
  }
@@ -211,19 +215,19 @@ function shouldIgnore(fullPath, name, relativePath, patterns) {
211
215
  for (const pattern of patterns) {
212
216
  if (pattern.includes('*')) {
213
217
  if (matchWildcard(pattern, name)) return true;
214
- if (matchWildcard(pattern, relativePath)) return true;
218
+ if (matchWildcard(pattern, normalizedRelative)) return true;
215
219
  }
216
220
 
217
221
  if (pattern.endsWith('/')) {
218
222
  const dir = pattern.slice(0, -1);
219
- if (fullPath.includes(path.sep + dir + path.sep) ||
220
- fullPath.includes(dir + path.sep) ||
223
+ if (normalizedFull.includes('/' + dir + '/') ||
224
+ normalizedFull.endsWith('/' + dir) ||
221
225
  name === dir) {
222
226
  return true;
223
227
  }
224
228
  }
225
229
 
226
- if (relativePath === pattern || name === pattern) {
230
+ if (normalizedRelative === pattern || name === pattern) {
227
231
  return true;
228
232
  }
229
233
  }
@@ -231,9 +235,16 @@ function shouldIgnore(fullPath, name, relativePath, patterns) {
231
235
  return false;
232
236
  }
233
237
 
234
- function getFiles(srcPath, config) {
235
- const files = [];
238
+ // ============================================
239
+ // SCAN AND BUILD JSON IN ONE PASS
240
+ // ============================================
241
+
242
+ function scanAndBuildJson(srcPath, config) {
243
+ const jsonArray = [];
236
244
  const ignorePatterns = parseIgnorePatterns(config.ignore);
245
+ let totalSize = 0;
246
+ let totalLines = 0;
247
+ const filesByExt = {};
237
248
 
238
249
  function walk(dir) {
239
250
  if (!fs.existsSync(dir)) return;
@@ -256,83 +267,175 @@ function getFiles(srcPath, config) {
256
267
  if (entry.isDirectory()) {
257
268
  walk(fullPath);
258
269
  } else if (entry.isFile() && isTextFile(entry.name)) {
259
- files.push({
260
- fullPath,
261
- relativePath,
270
+ // Read file immediately when found
271
+ let content;
272
+ try {
273
+ content = fs.readFileSync(fullPath, 'utf-8');
274
+ } catch (err) {
275
+ // Skip files that can't be read
276
+ continue;
277
+ }
278
+
279
+ const ext = path.extname(entry.name).slice(1).toLowerCase() || null;
280
+ const lines = content.split('\n').length;
281
+ const size = Buffer.byteLength(content, 'utf-8');
282
+ const language = getLanguage(entry.name);
283
+
284
+ // Update stats
285
+ totalSize += size;
286
+ totalLines += lines;
287
+ filesByExt[ext || 'other'] = (filesByExt[ext || 'other'] || 0) + 1;
288
+
289
+ // Add to JSON array immediately
290
+ jsonArray.push({
291
+ path: normalizePath(relativePath),
262
292
  name: entry.name,
263
- ext: path.extname(entry.name).slice(1).toLowerCase()
293
+ extension: ext,
294
+ language: language,
295
+ lines: lines,
296
+ size: size,
297
+ content: content
264
298
  });
265
299
  }
266
300
  }
267
301
  }
268
302
 
269
303
  walk(srcPath);
270
- return files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
304
+
305
+ // Sort by path for consistency
306
+ jsonArray.sort((a, b) => a.path.localeCompare(b.path));
307
+
308
+ const stats = {
309
+ files: jsonArray.length,
310
+ totalSize,
311
+ totalLines,
312
+ filesByExt
313
+ };
314
+
315
+ return { jsonArray, stats };
271
316
  }
272
317
 
273
- function buildContextContent(files, config, srcPath) {
318
+ // ============================================
319
+ // FORMAT CONVERTERS (from base JSON)
320
+ // ============================================
321
+
322
+ function toJson(baseJson) {
323
+ return JSON.stringify(baseJson, null, 2);
324
+ }
325
+
326
+ function toMarkdown(baseJson, config) {
274
327
  let content = '';
275
- let totalSize = 0;
276
- let totalLines = 0;
277
- const filesByExt = {};
278
328
 
279
329
  if (config.first_comment) {
280
330
  content += config.first_comment + '\n\n';
281
331
  }
282
332
 
333
+ // Project structure
283
334
  content += '## Project Structure\n\n```\n';
284
335
  const dirs = new Set();
285
- files.forEach(f => {
286
- const dir = path.dirname(f.relativePath);
336
+ baseJson.forEach(f => {
337
+ const dir = path.dirname(f.path);
287
338
  if (dir !== '.') dirs.add(dir);
288
339
  });
289
- dirs.forEach(d => content += `šŸ“ ${d}/\n`);
290
- content += `\n${files.length} files total\n\`\`\`\n\n`;
340
+ Array.from(dirs).sort().forEach(d => content += `šŸ“ ${d}/\n`);
341
+ content += `\n${baseJson.length} files total\n\`\`\`\n\n`;
291
342
 
343
+ // Source files
292
344
  content += '## Source Files\n\n';
293
345
 
294
- for (const file of files) {
295
- let fileContent;
296
- try {
297
- fileContent = fs.readFileSync(file.fullPath, 'utf-8');
298
- } catch (err) {
299
- console.log(chalk.yellow(`āš ļø Could not read: ${file.relativePath}`));
300
- continue;
346
+ for (const file of baseJson) {
347
+ content += `### ${file.path}\n\n`;
348
+ content += '```' + file.language + '\n';
349
+ content += file.content;
350
+ if (!file.content.endsWith('\n')) {
351
+ content += '\n';
301
352
  }
353
+ content += '```\n\n';
354
+ }
302
355
 
303
- const lang = getLanguage(file.name);
304
- const lines = fileContent.split('\n').length;
356
+ if (config.last_comment) {
357
+ content += config.last_comment;
358
+ }
305
359
 
306
- totalSize += Buffer.byteLength(fileContent, 'utf-8');
307
- totalLines += lines;
308
- filesByExt[file.ext || 'other'] = (filesByExt[file.ext || 'other'] || 0) + 1;
360
+ return content;
361
+ }
309
362
 
310
- content += `### ${file.relativePath}\n\n`;
311
- content += '```' + lang + '\n';
312
- content += fileContent;
363
+ function toToon(baseJson, stats) {
364
+ let content = '';
313
365
 
314
- if (!fileContent.endsWith('\n')) {
315
- content += '\n';
316
- }
366
+ // Meta header
367
+ content += `# Project Context\n`;
368
+ content += `# Generated: ${new Date().toISOString()}\n`;
369
+ content += `# Files: ${baseJson.length}\n`;
370
+ content += `# Lines: ${stats.totalLines}\n`;
371
+ content += `# Size: ${stats.totalSize} bytes\n\n`;
372
+
373
+ // Files table (compact tabular format - TOON's strength)
374
+ content += `files[${baseJson.length}]{path,name,extension,language,lines,size}:\n`;
375
+ for (const file of baseJson) {
376
+ const ext = file.extension || '';
377
+ content += ` ${escapeToonValue(file.path)},${escapeToonValue(file.name)},${ext},${file.language},${file.lines},${file.size}\n`;
378
+ }
317
379
 
318
- content += '```\n\n';
380
+ content += '\n';
381
+
382
+ // File contents
383
+ for (let i = 0; i < baseJson.length; i++) {
384
+ const file = baseJson[i];
385
+ content += `---\n`;
386
+ content += `[${i}] ${file.path}\n`;
387
+ content += `language: ${file.language}\n`;
388
+ content += `content:\n`;
389
+ // Indent each line with 2 spaces
390
+ const lines = file.content.split('\n');
391
+ for (const line of lines) {
392
+ content += ` ${line}\n`;
393
+ }
319
394
  }
320
395
 
321
- if (config.last_comment) {
322
- content += config.last_comment;
396
+ return content;
397
+ }
398
+
399
+ function escapeToonValue(value) {
400
+ if (value === null || value === undefined) return '';
401
+ const str = String(value);
402
+ if (str.includes(',') || str.includes('\n') || str.includes('"') ||
403
+ str.startsWith(' ') || str.endsWith(' ')) {
404
+ return '"' + str.replace(/"/g, '""').replace(/\n/g, '\\n') + '"';
323
405
  }
406
+ return str;
407
+ }
324
408
 
325
- contextStats = {
326
- files: files.length,
327
- totalSize,
328
- totalLines,
329
- filesByExt,
330
- estimatedTokens: estimateTokens(content)
331
- };
409
+ function toXml(baseJson) {
410
+ let content = '<?xml version="1.0" encoding="UTF-8"?>\n';
411
+ content += '<context>\n';
412
+
413
+ for (const file of baseJson) {
414
+ content += ` <file>\n`;
415
+ content += ` <path>${escapeXml(file.path)}</path>\n`;
416
+ content += ` <name>${escapeXml(file.name)}</name>\n`;
417
+ content += ` <extension>${escapeXml(file.extension || '')}</extension>\n`;
418
+ content += ` <language>${escapeXml(file.language)}</language>\n`;
419
+ content += ` <lines>${file.lines}</lines>\n`;
420
+ content += ` <size>${file.size}</size>\n`;
421
+ content += ` <content><![CDATA[\n${file.content}${file.content.endsWith('\n') ? '' : '\n'}]]></content>\n`;
422
+ content += ` </file>\n`;
423
+ }
332
424
 
425
+ content += '</context>\n';
333
426
  return content;
334
427
  }
335
428
 
429
+ function escapeXml(str) {
430
+ if (str === null || str === undefined) return '';
431
+ return String(str)
432
+ .replace(/&/g, '&amp;')
433
+ .replace(/</g, '&lt;')
434
+ .replace(/>/g, '&gt;')
435
+ .replace(/"/g, '&quot;')
436
+ .replace(/'/g, '&apos;');
437
+ }
438
+
336
439
  // ============================================
337
440
  // CONTEXT GENERATION
338
441
  // ============================================
@@ -356,45 +459,42 @@ async function generateContextDynamic() {
356
459
  ]);
357
460
 
358
461
  const config = { ...DEFAULT_PROJECT_CONFIG, src: srcPath };
359
- return generateContextFromConfig(config, srcPath);
462
+ return generateContext(config, srcPath);
360
463
  }
361
464
 
362
465
  async function generateContextFromConfigFile() {
363
466
  loadDependencies();
364
-
467
+
365
468
  const config = loadProjectConfig();
366
469
  if (!config) {
367
470
  console.log(chalk.yellow('\nāš ļø No config file found.'));
368
471
  return null;
369
472
  }
370
473
 
371
- return generateContextFromConfig(config, config.src);
474
+ return generateContext(config, config.src);
372
475
  }
373
476
 
374
- function generateContextFromConfig(config, srcPath) {
375
- const spinner = ora(`Scanning ${srcPath}...`).start();
477
+ function generateContext(config, srcPath) {
478
+ const spinner = ora(`Scanning and reading files from ${srcPath}...`).start();
376
479
 
377
480
  if (!fs.existsSync(srcPath)) {
378
481
  spinner.fail(`Source path does not exist: ${srcPath}`);
379
482
  return null;
380
483
  }
381
484
 
382
- contextFiles = getFiles(srcPath, config);
485
+ // Single pass: scan AND build JSON at the same time
486
+ const { jsonArray, stats } = scanAndBuildJson(srcPath, config);
383
487
 
384
- if (contextFiles.length === 0) {
488
+ if (jsonArray.length === 0) {
385
489
  spinner.fail(`No files found in: ${srcPath}`);
386
490
  return null;
387
491
  }
388
492
 
389
- spinner.text = `Building context from ${contextFiles.length} files...`;
390
- generatedContext = buildContextContent(contextFiles, config, srcPath);
391
-
392
- spinner.succeed(`Context generated: ${chalk.yellow(contextFiles.length)} files, ${chalk.yellow(formatSize(contextStats.totalSize))}`);
493
+ spinner.succeed(`Context built: ${chalk.yellow(jsonArray.length)} files, ${chalk.yellow(formatSize(stats.totalSize))}`);
393
494
 
394
495
  return {
395
- content: generatedContext,
396
- files: contextFiles,
397
- stats: contextStats,
496
+ baseJson: jsonArray,
497
+ stats: stats,
398
498
  config
399
499
  };
400
500
  }
@@ -403,36 +503,113 @@ function generateContextFromConfig(config, srcPath) {
403
503
  // SAVE CONTEXT
404
504
  // ============================================
405
505
 
406
- async function saveContext(result) {
506
+ // ============================================
507
+ // SAVE CONTEXT
508
+ // ============================================
509
+
510
+ async function saveContext(result, formats) {
407
511
  loadDependencies();
408
512
 
409
- let outputPath;
410
-
411
- if (result.config.output) {
412
- outputPath = result.config.output;
413
- } else {
414
- const { savePath } = await inquirer.prompt([
415
- {
416
- type: 'input',
417
- name: 'savePath',
418
- message: 'Enter output directory:',
419
- default: './mkctx'
420
- }
421
- ]);
422
- outputPath = savePath;
423
- }
513
+ const { fileName } = await inquirer.prompt([
514
+ {
515
+ type: 'input',
516
+ name: 'fileName',
517
+ message: 'Enter a name for the output files:',
518
+ default: 'context'
519
+ }
520
+ ]);
521
+
522
+ let outputPath = result.config.output || './mkctx';
424
523
 
425
524
  if (!fs.existsSync(outputPath)) {
426
525
  fs.mkdirSync(outputPath, { recursive: true });
427
526
  }
428
527
 
429
- const outputFile = path.join(outputPath, 'context.md');
430
- fs.writeFileSync(outputFile, result.content);
528
+ const savedFiles = [];
529
+
530
+ for (const format of formats) {
531
+ let content;
532
+ let filename;
533
+
534
+ switch (format) {
535
+ case 'json':
536
+ content = toJson(result.baseJson);
537
+ filename = `${fileName}.json`;
538
+ break;
539
+ case 'md':
540
+ content = toMarkdown(result.baseJson, result.config);
541
+ filename = `${fileName}.md`;
542
+ break;
543
+ case 'toon':
544
+ content = toToon(result.baseJson, result.stats);
545
+ filename = `${fileName}.toon`;
546
+ break;
547
+ case 'xml':
548
+ content = toXml(result.baseJson);
549
+ filename = `${fileName}.xml`;
550
+ break;
551
+ }
552
+
553
+ const outputFile = path.join(outputPath, filename);
554
+ fs.writeFileSync(outputFile, content);
555
+ const size = Buffer.byteLength(content, 'utf-8');
556
+ const tokens = estimateTokens(content);
557
+ savedFiles.push({ format, file: outputFile, size, tokens });
558
+ }
559
+
560
+ console.log(chalk.green('\nāœ… Context saved:\n'));
561
+ for (const { format, file, size, tokens } of savedFiles) {
562
+ console.log(chalk.white(` ${chalk.cyan(format.toUpperCase().padEnd(4))} → ${chalk.yellow(file)}`));
563
+ console.log(chalk.gray(` ${formatSize(size)} | ~${tokens.toLocaleString()} tokens\n`));
564
+ }
565
+
566
+ return savedFiles;
567
+ }
568
+
569
+ // ============================================
570
+ // FORMAT SELECTION
571
+ // ============================================
572
+
573
+ async function selectFormat() {
574
+ loadDependencies();
575
+
576
+ const { format } = await inquirer.prompt([
577
+ {
578
+ type: 'list',
579
+ name: 'format',
580
+ message: 'Select output format:',
581
+ default: 'all',
582
+ choices: [
583
+ {
584
+ name: chalk.magenta('šŸ“¦ All formats (MD, JSON, TOON, XML)'),
585
+ value: 'all'
586
+ },
587
+ new inquirer.Separator(),
588
+ {
589
+ name: chalk.blue('šŸ“ Markdown (.md)'),
590
+ value: 'md'
591
+ },
592
+ {
593
+ name: chalk.green('šŸ”§ JSON (.json) - Simple array'),
594
+ value: 'json'
595
+ },
596
+ {
597
+ name: chalk.yellow('šŸŽ’ TOON (.toon) - Token-optimized'),
598
+ value: 'toon'
599
+ },
600
+ {
601
+ name: chalk.red('šŸ“„ XML (.xml)'),
602
+ value: 'xml'
603
+ }
604
+ ]
605
+ }
606
+ ]);
431
607
 
432
- console.log(chalk.green(`\nāœ… Context saved to: ${chalk.yellow(outputFile)}`));
433
- console.log(chalk.gray(` ${result.stats.files} files, ${formatSize(result.stats.totalSize)}\n`));
608
+ if (format === 'all') {
609
+ return ['json', 'md', 'toon', 'xml'];
610
+ }
434
611
 
435
- return outputFile;
612
+ return [format];
436
613
  }
437
614
 
438
615
  // ============================================
@@ -464,8 +641,8 @@ async function showMainMenu() {
464
641
  },
465
642
  new inquirer.Separator(),
466
643
  {
467
- name: hasConfig
468
- ? chalk.gray('āš™ļø View configuration')
644
+ name: hasConfig
645
+ ? chalk.gray('āš™ļø View configuration')
469
646
  : chalk.yellow('āš™ļø Create configuration file'),
470
647
  value: 'config'
471
648
  },
@@ -488,41 +665,6 @@ async function showMainMenu() {
488
665
  return action;
489
666
  }
490
667
 
491
- async function showPostGenerationMenu(result) {
492
- loadDependencies();
493
-
494
- console.log(chalk.cyan('\nšŸ“Š Context Summary:'));
495
- console.log(chalk.white(` Files: ${result.stats.files}`));
496
- console.log(chalk.white(` Lines: ${result.stats.totalLines.toLocaleString()}`));
497
- console.log(chalk.white(` Size: ${formatSize(result.stats.totalSize)}`));
498
- console.log(chalk.white(` Est. tokens: ~${result.stats.estimatedTokens.toLocaleString()}`));
499
-
500
- const { action } = await inquirer.prompt([
501
- {
502
- type: 'list',
503
- name: 'action',
504
- message: 'What would you like to do with this context?',
505
- choices: [
506
- {
507
- name: chalk.blue('šŸ’¾ Save context to file'),
508
- value: 'save'
509
- },
510
- new inquirer.Separator(),
511
- {
512
- name: chalk.gray('šŸ”™ Back to main menu'),
513
- value: 'back'
514
- },
515
- {
516
- name: chalk.red('āŒ Exit'),
517
- value: 'exit'
518
- }
519
- ]
520
- }
521
- ]);
522
-
523
- return action;
524
- }
525
-
526
668
  // ============================================
527
669
  // MAIN APPLICATION
528
670
  // ============================================
@@ -557,18 +699,16 @@ async function main() {
557
699
  case 'from-config': {
558
700
  const result = await generateContextFromConfigFile();
559
701
  if (result) {
560
- let postAction = await showPostGenerationMenu(result);
561
-
562
- while (postAction !== 'back' && postAction !== 'exit') {
563
- if (postAction === 'save') {
564
- await saveContext(result);
565
- postAction = await showPostGenerationMenu(result);
566
- }
567
- }
568
-
569
- if (postAction === 'exit') {
570
- running = false;
571
- }
702
+ console.log(chalk.cyan('\nšŸ“Š Context Summary:'));
703
+ console.log(chalk.white(` Files: ${result.stats.files}`));
704
+ console.log(chalk.white(` Lines: ${result.stats.totalLines.toLocaleString()}`));
705
+ console.log(chalk.white(` Size: ${formatSize(result.stats.totalSize)}`));
706
+
707
+ const formats = await selectFormat();
708
+ await saveContext(result, formats);
709
+
710
+ console.log(chalk.yellow('šŸ‘‹ Done!\n'));
711
+ running = false;
572
712
  }
573
713
  break;
574
714
  }
@@ -576,18 +716,16 @@ async function main() {
576
716
  case 'dynamic': {
577
717
  const result = await generateContextDynamic();
578
718
  if (result) {
579
- let postAction = await showPostGenerationMenu(result);
580
-
581
- while (postAction !== 'back' && postAction !== 'exit') {
582
- if (postAction === 'save') {
583
- await saveContext(result);
584
- postAction = await showPostGenerationMenu(result);
585
- }
586
- }
587
-
588
- if (postAction === 'exit') {
589
- running = false;
590
- }
719
+ console.log(chalk.cyan('\nšŸ“Š Context Summary:'));
720
+ console.log(chalk.white(` Files: ${result.stats.files}`));
721
+ console.log(chalk.white(` Lines: ${result.stats.totalLines.toLocaleString()}`));
722
+ console.log(chalk.white(` Size: ${formatSize(result.stats.totalSize)}`));
723
+
724
+ const formats = await selectFormat();
725
+ await saveContext(result, formats);
726
+
727
+ console.log(chalk.yellow('šŸ‘‹ Done!\n'));
728
+ running = false;
591
729
  }
592
730
  break;
593
731
  }
@@ -613,7 +751,7 @@ async function main() {
613
751
 
614
752
  function showHelp() {
615
753
  loadDependencies();
616
-
754
+
617
755
  console.log(chalk.cyan(`
618
756
  ╔════════════════════════════════════════════════════════════╗
619
757
  ā•‘ šŸ“„ mkctx - Make Context for AI Code Analysis ā•‘
@@ -627,12 +765,22 @@ function showHelp() {
627
765
  mkctx help Show this help message
628
766
  mkctx version Show version
629
767
 
630
- ${chalk.yellow('Configuration (mkctx.config.json):')}
631
- src Source directory to scan (default: ".")
632
- ignore Comma-separated patterns to ignore
633
- output Output directory (default: "./mkctx")
634
- first_comment Comment at the beginning of context
635
- last_comment Comment at the end of context
768
+ ${chalk.yellow('Output Formats:')}
769
+ ${chalk.green('JSON')} Simple array of file objects (base format)
770
+ ${chalk.blue('MD')} Markdown with code blocks
771
+ ${chalk.yellow('TOON')} Token-Oriented Object Notation (LLM optimized)
772
+ ${chalk.red('XML')} XML with CDATA sections
773
+
774
+ ${chalk.yellow('JSON Structure:')}
775
+ [{
776
+ "path": "src/index.ts",
777
+ "name": "index.ts",
778
+ "extension": "ts",
779
+ "language": "typescript",
780
+ "lines": 150,
781
+ "size": 4096,
782
+ "content": "..."
783
+ }]
636
784
 
637
785
  ${chalk.gray('More info: https://github.com/pnkkzero/mkctx')}
638
786
  `));
@@ -643,7 +791,7 @@ function showVersion() {
643
791
  const pkg = require('./package.json');
644
792
  console.log(`mkctx v${pkg.version}`);
645
793
  } catch {
646
- console.log('mkctx v3.0.0');
794
+ console.log('mkctx v4.0.0');
647
795
  }
648
796
  }
649
797
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mkctx",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "Generate markdown context files from your project code and chat with AI using Ollama",
5
5
  "main": "bin/mkctx.js",
6
6
  "bin": {