roguelike-cli 1.2.6 → 1.3.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.
@@ -41,60 +41,60 @@ const child_process_1 = require("child_process");
41
41
  const storage_1 = require("../storage/storage");
42
42
  const nodeConfig_1 = require("../storage/nodeConfig");
43
43
  const claude_1 = require("../ai/claude");
44
+ const profile_1 = require("../storage/profile");
44
45
  // Parse tree ASCII art and create folder structure
45
46
  function createFoldersFromTree(rootPath, treeContent) {
46
- // Create root folder
47
47
  if (!fs.existsSync(rootPath)) {
48
48
  fs.mkdirSync(rootPath, { recursive: true });
49
49
  }
50
- // Parse tree lines
51
50
  const lines = treeContent.split('\n');
52
51
  const stack = [{ path: rootPath, indent: -1 }];
53
52
  for (const line of lines) {
54
- // Skip empty lines
55
53
  if (!line.trim())
56
54
  continue;
57
- // Extract node name from tree line
58
- // Patterns: "├── Name", "└── Name", "│ ├── Name", etc.
59
55
  const match = line.match(/^([\s│]*)[├└]──\s*(.+)$/);
60
56
  if (!match)
61
57
  continue;
62
58
  const prefix = match[1];
63
59
  let nodeName = match[2].trim();
64
- // Calculate indent level (each │ or space block = 1 level)
65
60
  const indent = Math.floor(prefix.replace(/│/g, ' ').length / 4);
66
- // Clean node name - remove extra info in parentheses, brackets
61
+ // Extract metadata from node name
62
+ const isBoss = /\[BOSS\]/i.test(nodeName) || /\[MILESTONE\]/i.test(nodeName);
63
+ const deadlineMatch = nodeName.match(/\[(?:DUE|DEADLINE):\s*([^\]]+)\]/i);
64
+ const deadline = deadlineMatch ? deadlineMatch[1].trim() : undefined;
65
+ // Clean node name
67
66
  nodeName = nodeName.replace(/\s*\([^)]*\)\s*/g, '').trim();
68
67
  nodeName = nodeName.replace(/\s*\[[^\]]*\]\s*/g, '').trim();
69
- // Create safe folder name
70
68
  const safeName = nodeName
71
69
  .toLowerCase()
72
70
  .replace(/[^a-z0-9]+/g, '-')
73
71
  .replace(/^-+|-+$/g, '');
74
72
  if (!safeName)
75
73
  continue;
76
- // Pop stack until we find parent
77
74
  while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
78
75
  stack.pop();
79
76
  }
80
77
  const parentPath = stack[stack.length - 1].path;
81
78
  const folderPath = path.join(parentPath, safeName);
82
- // Create folder
83
79
  if (!fs.existsSync(folderPath)) {
84
80
  fs.mkdirSync(folderPath, { recursive: true });
85
81
  }
86
- // Create node config
82
+ // Calculate depth for XP
83
+ const depth = stack.length;
87
84
  (0, nodeConfig_1.writeNodeConfig)(folderPath, {
88
85
  name: nodeName,
86
+ status: 'open',
87
+ xp: (0, nodeConfig_1.calculateXP)(depth, isBoss),
88
+ isBoss,
89
+ deadline,
89
90
  createdAt: new Date().toISOString(),
90
91
  updatedAt: new Date().toISOString(),
91
92
  });
92
- // Push to stack
93
93
  stack.push({ path: folderPath, indent });
94
94
  }
95
95
  }
96
96
  // Generate dungeon map visualization from folder structure
97
- function generateDungeonMap(dirPath) {
97
+ function generateDungeonMap(dirPath, config) {
98
98
  if (!fs.existsSync(dirPath)) {
99
99
  return 'Directory does not exist.';
100
100
  }
@@ -106,54 +106,64 @@ function generateDungeonMap(dirPath) {
106
106
  const lines = [];
107
107
  const roomWidth = 20;
108
108
  const roomsPerRow = 2;
109
- const wall = '';
109
+ const wall = '#';
110
110
  const door = '+';
111
111
  const task = '*';
112
112
  const milestone = '@';
113
- // Group folders into rows of 2
113
+ const done = 'x';
114
+ const blocked = '!';
114
115
  const rows = [];
115
116
  for (let i = 0; i < folders.length; i += roomsPerRow) {
116
117
  rows.push(folders.slice(i, i + roomsPerRow));
117
118
  }
118
- // Top border
119
+ lines.push('');
119
120
  lines.push(' ' + wall.repeat(roomWidth * roomsPerRow + 3));
120
121
  rows.forEach((row, rowIndex) => {
121
- // Room content
122
122
  for (let line = 0; line < 6; line++) {
123
123
  let rowStr = ' ' + wall;
124
124
  row.forEach((folder, colIndex) => {
125
- const name = folder.name.replace(/-/g, ' ');
125
+ const folderPath = path.join(dirPath, folder.name);
126
+ const nodeConfig = (0, nodeConfig_1.readNodeConfig)(folderPath);
127
+ const name = (nodeConfig?.name || folder.name).replace(/-/g, ' ');
126
128
  const displayName = name.length > roomWidth - 4
127
129
  ? name.substring(0, roomWidth - 7) + '...'
128
130
  : name;
129
- // Get sub-items
130
131
  const subPath = path.join(dirPath, folder.name);
131
132
  const subEntries = fs.existsSync(subPath)
132
133
  ? fs.readdirSync(subPath, { withFileTypes: true })
133
134
  .filter(e => e.isDirectory() && !e.name.startsWith('.'))
134
135
  : [];
135
136
  if (line === 0) {
136
- // Empty line
137
137
  rowStr += ' '.repeat(roomWidth - 1) + wall;
138
138
  }
139
139
  else if (line === 1) {
140
- // Room name
140
+ const statusIcon = nodeConfig?.status === 'done' ? '[DONE]'
141
+ : nodeConfig?.isBoss ? '[BOSS]'
142
+ : '';
143
+ const title = statusIcon ? `${statusIcon}` : `[${displayName}]`;
144
+ const padding = roomWidth - title.length - 1;
145
+ rowStr += ' ' + title + ' '.repeat(Math.max(0, padding - 1)) + wall;
146
+ }
147
+ else if (line === 2 && !nodeConfig?.isBoss) {
141
148
  const title = `[${displayName}]`;
142
149
  const padding = roomWidth - title.length - 1;
143
150
  rowStr += ' ' + title + ' '.repeat(Math.max(0, padding - 1)) + wall;
144
151
  }
145
152
  else if (line >= 2 && line <= 4) {
146
- // Sub-items
147
- const itemIndex = line - 2;
148
- if (itemIndex < subEntries.length) {
149
- const subName = subEntries[itemIndex].name.replace(/-/g, ' ');
153
+ const itemIndex = nodeConfig?.isBoss ? line - 2 : line - 3;
154
+ if (itemIndex >= 0 && itemIndex < subEntries.length) {
155
+ const subConfig = (0, nodeConfig_1.readNodeConfig)(path.join(subPath, subEntries[itemIndex].name));
156
+ const subName = (subConfig?.name || subEntries[itemIndex].name).replace(/-/g, ' ');
150
157
  const shortName = subName.length > roomWidth - 6
151
158
  ? subName.substring(0, roomWidth - 9) + '...'
152
159
  : subName;
153
- const marker = subName.toLowerCase().includes('boss') ||
154
- subName.toLowerCase().includes('launch') ||
155
- subName.toLowerCase().includes('deploy')
156
- ? milestone : task;
160
+ let marker = task;
161
+ if (subConfig?.status === 'done')
162
+ marker = done;
163
+ else if (subConfig?.status === 'blocked')
164
+ marker = blocked;
165
+ else if (subConfig?.isBoss)
166
+ marker = milestone;
157
167
  const itemStr = `${marker} ${shortName}`;
158
168
  const itemPadding = roomWidth - itemStr.length - 1;
159
169
  rowStr += ' ' + itemStr + ' '.repeat(Math.max(0, itemPadding - 1)) + wall;
@@ -163,10 +173,8 @@ function generateDungeonMap(dirPath) {
163
173
  }
164
174
  }
165
175
  else {
166
- // Empty line
167
176
  rowStr += ' '.repeat(roomWidth - 1) + wall;
168
177
  }
169
- // Add door between rooms
170
178
  if (colIndex < row.length - 1 && line === 3) {
171
179
  rowStr = rowStr.slice(0, -1) + door + door + door;
172
180
  }
@@ -174,21 +182,18 @@ function generateDungeonMap(dirPath) {
174
182
  rowStr = rowStr.slice(0, -1) + wall;
175
183
  }
176
184
  });
177
- // Fill empty space if odd number of rooms
178
185
  if (row.length < roomsPerRow) {
179
186
  rowStr += ' '.repeat(roomWidth) + wall;
180
187
  }
181
188
  lines.push(rowStr);
182
189
  }
183
- // Bottom border with doors to next row
184
190
  if (rowIndex < rows.length - 1) {
185
191
  let borderStr = ' ' + wall.repeat(Math.floor(roomWidth / 2)) + door;
186
192
  borderStr += wall.repeat(roomWidth - 1) + door;
187
193
  borderStr += wall.repeat(Math.floor(roomWidth / 2) + 1);
188
194
  lines.push(borderStr);
189
- // Corridor
190
- let corridorStr = ' ' + ' '.repeat(Math.floor(roomWidth / 2)) + '';
191
- corridorStr += ' '.repeat(roomWidth - 1) + '│';
195
+ let corridorStr = ' ' + ' '.repeat(Math.floor(roomWidth / 2)) + '|';
196
+ corridorStr += ' '.repeat(roomWidth - 1) + '|';
192
197
  lines.push(corridorStr);
193
198
  borderStr = ' ' + wall.repeat(Math.floor(roomWidth / 2)) + door;
194
199
  borderStr += wall.repeat(roomWidth - 1) + door;
@@ -196,19 +201,94 @@ function generateDungeonMap(dirPath) {
196
201
  lines.push(borderStr);
197
202
  }
198
203
  });
199
- // Bottom border
200
204
  lines.push(' ' + wall.repeat(roomWidth * roomsPerRow + 3));
201
- // Legend
202
205
  lines.push('');
203
- lines.push(`Legend: ${task} Task ${milestone} Milestone ${door} Door ${wall} Wall`);
206
+ lines.push(`Legend: ${task} Task ${done} Done ${milestone} Boss/Milestone ${blocked} Blocked ${door} Door`);
204
207
  return lines.join('\n');
205
208
  }
206
- // Global session state
209
+ // Parse human-readable date
210
+ function parseDeadline(input) {
211
+ const lower = input.toLowerCase().trim();
212
+ const today = new Date();
213
+ if (lower === 'today') {
214
+ return today.toISOString().split('T')[0];
215
+ }
216
+ if (lower === 'tomorrow') {
217
+ today.setDate(today.getDate() + 1);
218
+ return today.toISOString().split('T')[0];
219
+ }
220
+ // +Nd format (e.g., +3d, +7d)
221
+ const plusDaysMatch = lower.match(/^\+(\d+)d$/);
222
+ if (plusDaysMatch) {
223
+ today.setDate(today.getDate() + parseInt(plusDaysMatch[1]));
224
+ return today.toISOString().split('T')[0];
225
+ }
226
+ // Try parsing as date
227
+ const parsed = new Date(input);
228
+ if (!isNaN(parsed.getTime())) {
229
+ return parsed.toISOString().split('T')[0];
230
+ }
231
+ return null;
232
+ }
233
+ // Format deadline for display
234
+ function formatDeadline(deadline) {
235
+ const deadlineDate = new Date(deadline);
236
+ const today = new Date();
237
+ today.setHours(0, 0, 0, 0);
238
+ deadlineDate.setHours(0, 0, 0, 0);
239
+ const diffDays = Math.ceil((deadlineDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
240
+ if (diffDays < 0)
241
+ return `OVERDUE ${Math.abs(diffDays)}d`;
242
+ if (diffDays === 0)
243
+ return 'TODAY';
244
+ if (diffDays === 1)
245
+ return 'tomorrow';
246
+ if (diffDays <= 7)
247
+ return `${diffDays}d left`;
248
+ return deadlineDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
249
+ }
250
+ // Get depth of current path relative to storage root
251
+ function getDepth(currentPath, storagePath) {
252
+ const relative = path.relative(storagePath, currentPath);
253
+ if (!relative)
254
+ return 0;
255
+ return relative.split(path.sep).length;
256
+ }
257
+ // Mark node as done recursively
258
+ function markDoneRecursive(nodePath, storagePath) {
259
+ let result = { xpGained: 0, tasksCompleted: 0, bossesDefeated: 0 };
260
+ const config = (0, nodeConfig_1.readNodeConfig)(nodePath);
261
+ if (!config || config.status === 'done') {
262
+ return result;
263
+ }
264
+ // First, mark all children as done
265
+ const entries = fs.readdirSync(nodePath, { withFileTypes: true });
266
+ for (const entry of entries) {
267
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
268
+ const childResult = markDoneRecursive(path.join(nodePath, entry.name), storagePath);
269
+ result.xpGained += childResult.xpGained;
270
+ result.tasksCompleted += childResult.tasksCompleted;
271
+ result.bossesDefeated += childResult.bossesDefeated;
272
+ }
273
+ }
274
+ // Mark this node as done
275
+ const depth = getDepth(nodePath, storagePath);
276
+ const xp = config.xp || (0, nodeConfig_1.calculateXP)(depth, config.isBoss || false);
277
+ (0, nodeConfig_1.writeNodeConfig)(nodePath, {
278
+ ...config,
279
+ status: 'done',
280
+ completedAt: new Date().toISOString(),
281
+ });
282
+ result.xpGained += xp;
283
+ result.tasksCompleted += 1;
284
+ if (config.isBoss)
285
+ result.bossesDefeated += 1;
286
+ return result;
287
+ }
207
288
  exports.sessionState = {
208
289
  pending: null,
209
290
  history: []
210
291
  };
211
- // Format items in columns like native ls
212
292
  function formatColumns(items, termWidth = 80) {
213
293
  if (items.length === 0)
214
294
  return '';
@@ -221,7 +301,6 @@ function formatColumns(items, termWidth = 80) {
221
301
  }
222
302
  return rows.join('\n');
223
303
  }
224
- // Copy to clipboard (cross-platform)
225
304
  function copyToClipboard(text) {
226
305
  const platform = process.platform;
227
306
  try {
@@ -232,7 +311,6 @@ function copyToClipboard(text) {
232
311
  (0, child_process_1.execSync)('clip', { input: text });
233
312
  }
234
313
  else {
235
- // Linux - try xclip or xsel
236
314
  try {
237
315
  (0, child_process_1.execSync)('xclip -selection clipboard', { input: text });
238
316
  }
@@ -242,10 +320,9 @@ function copyToClipboard(text) {
242
320
  }
243
321
  }
244
322
  catch (e) {
245
- // Silently fail if clipboard not available
323
+ // Silently fail
246
324
  }
247
325
  }
248
- // Helper function for recursive copy
249
326
  function copyRecursive(src, dest) {
250
327
  const stat = fs.statSync(src);
251
328
  if (stat.isDirectory()) {
@@ -263,14 +340,60 @@ function copyRecursive(src, dest) {
263
340
  fs.copyFileSync(src, dest);
264
341
  }
265
342
  }
343
+ // Build tree with status and deadline info
344
+ function getTreeWithStatus(dirPath, prefix = '', isRoot = true, maxDepth = 10, currentDepth = 0, showFiles = false) {
345
+ const lines = [];
346
+ if (!fs.existsSync(dirPath)) {
347
+ return lines;
348
+ }
349
+ if (currentDepth >= maxDepth) {
350
+ return lines;
351
+ }
352
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
353
+ const dirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.'));
354
+ const files = showFiles ? entries.filter(e => e.isFile() && !e.name.startsWith('.')) : [];
355
+ const allItems = [...dirs, ...files];
356
+ allItems.forEach((entry, index) => {
357
+ const isLast = index === allItems.length - 1;
358
+ const connector = isLast ? '└── ' : '├── ';
359
+ const nextPrefix = isLast ? ' ' : '│ ';
360
+ if (entry.isDirectory()) {
361
+ const nodePath = path.join(dirPath, entry.name);
362
+ const config = (0, nodeConfig_1.readNodeConfig)(nodePath);
363
+ let displayName = config?.name || entry.name;
364
+ const tags = [];
365
+ // Add status tag
366
+ if (config?.status === 'done') {
367
+ tags.push('DONE');
368
+ }
369
+ else if (config?.status === 'blocked') {
370
+ tags.push('BLOCKED');
371
+ }
372
+ // Add boss tag
373
+ if (config?.isBoss) {
374
+ tags.push('BOSS');
375
+ }
376
+ // Add deadline tag
377
+ if (config?.deadline && config.status !== 'done') {
378
+ tags.push(formatDeadline(config.deadline));
379
+ }
380
+ const tagStr = tags.length > 0 ? ` [${tags.join('] [')}]` : '';
381
+ lines.push(`${prefix}${connector}${displayName}/${tagStr}`);
382
+ const childLines = getTreeWithStatus(nodePath, prefix + nextPrefix, false, maxDepth, currentDepth + 1, showFiles);
383
+ lines.push(...childLines);
384
+ }
385
+ else {
386
+ lines.push(`${prefix}${connector}${entry.name}`);
387
+ }
388
+ });
389
+ return lines;
390
+ }
266
391
  async function processCommand(input, currentPath, config, signal, rl) {
267
- // Check for clipboard pipe
268
392
  const clipboardPipe = /\s*\|\s*(pbcopy|copy|clip)\s*$/i;
269
393
  const shouldCopy = clipboardPipe.test(input);
270
394
  const cleanInput = input.replace(clipboardPipe, '').trim();
271
395
  const parts = cleanInput.split(' ').filter(p => p.length > 0);
272
396
  const command = parts[0].toLowerCase();
273
- // Helper to wrap result with clipboard copy
274
397
  const wrapResult = (result) => {
275
398
  if (shouldCopy && result.output) {
276
399
  copyToClipboard(result.output);
@@ -283,9 +406,155 @@ async function processCommand(input, currentPath, config, signal, rl) {
283
406
  const pkg = require('../../package.json');
284
407
  return wrapResult({ output: `Roguelike CLI v${pkg.version}` });
285
408
  }
409
+ // Stats command
410
+ if (command === 'stats') {
411
+ return wrapResult({ output: (0, profile_1.formatStats)() });
412
+ }
413
+ // Achievements command
414
+ if (command === 'achievements' || command === 'ach') {
415
+ return wrapResult({ output: (0, profile_1.formatAchievements)() });
416
+ }
417
+ // Done command - mark current node as completed
418
+ if (command === 'done') {
419
+ const nodeConfig = (0, nodeConfig_1.readNodeConfig)(currentPath);
420
+ if (!nodeConfig) {
421
+ return wrapResult({ output: 'No task at current location. Navigate to a task first.' });
422
+ }
423
+ if (nodeConfig.status === 'done') {
424
+ return wrapResult({ output: 'This task is already completed.' });
425
+ }
426
+ // Mark done recursively
427
+ const result = markDoneRecursive(currentPath, config.storagePath);
428
+ // Update profile with XP and achievements
429
+ const depth = getDepth(currentPath, config.storagePath);
430
+ const taskResult = (0, profile_1.completeTask)(result.xpGained, nodeConfig.isBoss || false, depth, nodeConfig.createdAt);
431
+ let output = `\n=== TASK COMPLETED ===\n`;
432
+ output += `\nTasks completed: ${result.tasksCompleted}`;
433
+ if (result.bossesDefeated > 0) {
434
+ output += `\nBosses defeated: ${result.bossesDefeated}`;
435
+ }
436
+ output += `\n+${result.xpGained} XP`;
437
+ if (taskResult.levelUp) {
438
+ output += `\n\n*** LEVEL UP! ***`;
439
+ output += `\nYou are now level ${taskResult.newLevel}!`;
440
+ }
441
+ if (taskResult.newAchievements.length > 0) {
442
+ output += `\n\n=== NEW ACHIEVEMENTS ===`;
443
+ for (const ach of taskResult.newAchievements) {
444
+ output += `\n[x] ${ach.name}: ${ach.description}`;
445
+ }
446
+ }
447
+ output += '\n';
448
+ return wrapResult({ output });
449
+ }
450
+ // Deadline command
451
+ if (command === 'deadline') {
452
+ if (parts.length < 2) {
453
+ return wrapResult({ output: 'Usage: deadline <date>\nExamples: deadline today, deadline tomorrow, deadline +3d, deadline Jan 15' });
454
+ }
455
+ const dateStr = parts.slice(1).join(' ');
456
+ const deadline = parseDeadline(dateStr);
457
+ if (!deadline) {
458
+ return wrapResult({ output: `Could not parse date: ${dateStr}` });
459
+ }
460
+ const nodeConfig = (0, nodeConfig_1.readNodeConfig)(currentPath);
461
+ if (!nodeConfig) {
462
+ return wrapResult({ output: 'No task at current location.' });
463
+ }
464
+ (0, nodeConfig_1.writeNodeConfig)(currentPath, {
465
+ ...nodeConfig,
466
+ deadline,
467
+ });
468
+ return wrapResult({ output: `Deadline set: ${formatDeadline(deadline)}` });
469
+ }
470
+ // Boss command - mark as boss/milestone
471
+ if (command === 'boss' || command === 'milestone') {
472
+ const nodeConfig = (0, nodeConfig_1.readNodeConfig)(currentPath);
473
+ if (!nodeConfig) {
474
+ return wrapResult({ output: 'No task at current location.' });
475
+ }
476
+ const newIsBoss = !nodeConfig.isBoss;
477
+ const depth = getDepth(currentPath, config.storagePath);
478
+ (0, nodeConfig_1.writeNodeConfig)(currentPath, {
479
+ ...nodeConfig,
480
+ isBoss: newIsBoss,
481
+ xp: (0, nodeConfig_1.calculateXP)(depth, newIsBoss),
482
+ });
483
+ return wrapResult({ output: newIsBoss ? 'Marked as BOSS task (3x XP)' : 'Removed BOSS status' });
484
+ }
485
+ // Block command
486
+ if (command === 'block') {
487
+ const nodeConfig = (0, nodeConfig_1.readNodeConfig)(currentPath);
488
+ if (!nodeConfig) {
489
+ return wrapResult({ output: 'No task at current location.' });
490
+ }
491
+ const reason = parts.length > 1 ? parts.slice(1).join(' ') : undefined;
492
+ (0, nodeConfig_1.writeNodeConfig)(currentPath, {
493
+ ...nodeConfig,
494
+ status: 'blocked',
495
+ blockedBy: reason ? [reason] : nodeConfig.blockedBy,
496
+ });
497
+ return wrapResult({ output: reason ? `Blocked: ${reason}` : 'Task marked as blocked' });
498
+ }
499
+ // Unblock command
500
+ if (command === 'unblock') {
501
+ const nodeConfig = (0, nodeConfig_1.readNodeConfig)(currentPath);
502
+ if (!nodeConfig) {
503
+ return wrapResult({ output: 'No task at current location.' });
504
+ }
505
+ (0, nodeConfig_1.writeNodeConfig)(currentPath, {
506
+ ...nodeConfig,
507
+ status: 'open',
508
+ blockedBy: [],
509
+ });
510
+ return wrapResult({ output: 'Task unblocked' });
511
+ }
512
+ // Status command - show current task status
513
+ if (command === 'status') {
514
+ const nodeConfig = (0, nodeConfig_1.readNodeConfig)(currentPath);
515
+ if (!nodeConfig) {
516
+ return wrapResult({ output: 'No task at current location.' });
517
+ }
518
+ const lines = [
519
+ '',
520
+ `Task: ${nodeConfig.name}`,
521
+ `Status: ${nodeConfig.status.toUpperCase()}`,
522
+ `XP: ${nodeConfig.xp}`,
523
+ ];
524
+ if (nodeConfig.isBoss) {
525
+ lines.push('Type: BOSS');
526
+ }
527
+ if (nodeConfig.deadline) {
528
+ lines.push(`Deadline: ${formatDeadline(nodeConfig.deadline)}`);
529
+ }
530
+ if (nodeConfig.completedAt) {
531
+ lines.push(`Completed: ${new Date(nodeConfig.completedAt).toLocaleDateString()}`);
532
+ }
533
+ if (nodeConfig.blockedBy && nodeConfig.blockedBy.length > 0) {
534
+ lines.push(`Blocked by: ${nodeConfig.blockedBy.join(', ')}`);
535
+ }
536
+ lines.push('');
537
+ return wrapResult({ output: lines.join('\n') });
538
+ }
286
539
  // Map command - dungeon visualization
287
540
  if (command === 'map') {
288
- const dungeonMap = generateDungeonMap(currentPath);
541
+ // Check for --ai flag to use AI generation
542
+ if (parts.includes('--ai') || parts.includes('-a')) {
543
+ const treeLines = getTreeWithStatus(currentPath, '', true, 10, 0, false);
544
+ const treeContent = treeLines.join('\n');
545
+ if (!treeContent) {
546
+ return wrapResult({ output: 'No tasks to visualize.' });
547
+ }
548
+ const mapContent = await (0, claude_1.generateDungeonMapWithAI)(treeContent, config, signal);
549
+ if (mapContent) {
550
+ // Save map to file
551
+ const folderName = path.basename(currentPath);
552
+ (0, nodeConfig_1.saveMapFile)(currentPath, folderName + '-map', mapContent);
553
+ return wrapResult({ output: mapContent + '\n\n[Map saved as .rlc.map]' });
554
+ }
555
+ return wrapResult({ output: 'Could not generate AI map. Using default.' });
556
+ }
557
+ const dungeonMap = generateDungeonMap(currentPath, config);
289
558
  return wrapResult({ output: dungeonMap });
290
559
  }
291
560
  if (command === 'ls') {
@@ -298,7 +567,16 @@ async function processCommand(input, currentPath, config, signal, rl) {
298
567
  if (entry.name.startsWith('.'))
299
568
  continue;
300
569
  if (entry.isDirectory()) {
301
- items.push(entry.name + '/');
570
+ const nodePath = path.join(currentPath, entry.name);
571
+ const config = (0, nodeConfig_1.readNodeConfig)(nodePath);
572
+ let suffix = '/';
573
+ if (config?.status === 'done')
574
+ suffix = '/ [DONE]';
575
+ else if (config?.status === 'blocked')
576
+ suffix = '/ [BLOCKED]';
577
+ else if (config?.isBoss)
578
+ suffix = '/ [BOSS]';
579
+ items.push(entry.name + suffix);
302
580
  }
303
581
  else {
304
582
  items.push(entry.name);
@@ -312,7 +590,6 @@ async function processCommand(input, currentPath, config, signal, rl) {
312
590
  }
313
591
  if (command === 'tree') {
314
592
  const showFiles = parts.includes('-A') || parts.includes('--all');
315
- // Parse depth: --depth=N or -d N
316
593
  let maxDepth = 10;
317
594
  const depthFlag = parts.find(p => p.startsWith('--depth='));
318
595
  if (depthFlag) {
@@ -324,29 +601,25 @@ async function processCommand(input, currentPath, config, signal, rl) {
324
601
  maxDepth = parseInt(parts[dIndex + 1]) || 10;
325
602
  }
326
603
  }
327
- const treeLines = (0, storage_1.getTree)(currentPath, '', true, maxDepth, 0, showFiles);
604
+ const treeLines = getTreeWithStatus(currentPath, '', true, maxDepth, 0, showFiles);
328
605
  if (treeLines.length === 0) {
329
606
  return wrapResult({ output: 'No items found.' });
330
607
  }
331
608
  return wrapResult({ output: treeLines.join('\n') });
332
609
  }
333
- // Handle navigation without 'cd' command (.., ..., ...., etc)
610
+ // Handle navigation without 'cd' command
334
611
  if (/^\.{2,}$/.test(command)) {
335
- // Count dots: .. = 1 level, ... = 2 levels, .... = 3 levels, etc
336
612
  const levels = command.length - 1;
337
613
  let targetPath = currentPath;
338
- // Already at root?
339
614
  if (targetPath === config.storagePath) {
340
615
  return { output: 'Already at root.' };
341
616
  }
342
617
  for (let i = 0; i < levels; i++) {
343
618
  const parentPath = path.dirname(targetPath);
344
- // Stop at storage root
345
619
  if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
346
620
  break;
347
621
  }
348
622
  targetPath = parentPath;
349
- // If we reached root, stop
350
623
  if (targetPath === config.storagePath) {
351
624
  break;
352
625
  }
@@ -402,7 +675,6 @@ async function processCommand(input, currentPath, config, signal, rl) {
402
675
  return { output: `Moved: ${source} -> ${dest}` };
403
676
  }
404
677
  catch (error) {
405
- // If rename fails (cross-device), copy then delete
406
678
  try {
407
679
  copyRecursive(sourcePath, destPath);
408
680
  fs.rmSync(sourcePath, { recursive: true, force: true });
@@ -415,23 +687,19 @@ async function processCommand(input, currentPath, config, signal, rl) {
415
687
  }
416
688
  if (command === 'open') {
417
689
  const { exec } = require('child_process');
418
- // open or open . - open current folder in system file manager
419
690
  if (parts.length < 2 || parts[1] === '.') {
420
691
  exec(`open "${currentPath}"`);
421
692
  return { output: `Opening: ${currentPath}` };
422
693
  }
423
694
  const name = parts.slice(1).join(' ');
424
695
  const targetPath = path.join(currentPath, name);
425
- // Check if target exists
426
696
  if (fs.existsSync(targetPath)) {
427
697
  const stat = fs.statSync(targetPath);
428
698
  if (stat.isDirectory()) {
429
- // It's a folder, open in file manager
430
699
  exec(`open "${targetPath}"`);
431
700
  return { output: `Opening: ${targetPath}` };
432
701
  }
433
702
  if (stat.isFile()) {
434
- // It's a file, show its content (supports | pbcopy)
435
703
  const content = fs.readFileSync(targetPath, 'utf-8');
436
704
  return wrapResult({ output: content });
437
705
  }
@@ -478,35 +746,29 @@ async function processCommand(input, currentPath, config, signal, rl) {
478
746
  return { output: 'Usage: cd <node> or cd .. or cd <path>' };
479
747
  }
480
748
  const target = parts.slice(1).join(' ');
481
- // Handle cd .., cd ..., cd ...., etc
482
749
  if (/^\.{2,}$/.test(target)) {
483
750
  const levels = target.length - 1;
484
751
  let targetPath = currentPath;
485
- // Already at root?
486
752
  if (targetPath === config.storagePath) {
487
753
  return { output: 'Already at root.' };
488
754
  }
489
755
  for (let i = 0; i < levels; i++) {
490
756
  const parentPath = path.dirname(targetPath);
491
- // Stop at storage root
492
757
  if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
493
758
  break;
494
759
  }
495
760
  targetPath = parentPath;
496
- // If we reached root, stop
497
761
  if (targetPath === config.storagePath) {
498
762
  break;
499
763
  }
500
764
  }
501
765
  return { newPath: targetPath, output: '' };
502
766
  }
503
- // Handle paths like "cd bank/account" or "cd ../other"
504
767
  if (target.includes('/')) {
505
768
  let targetPath = currentPath;
506
769
  const pathParts = target.split('/');
507
770
  for (const part of pathParts) {
508
771
  if (/^\.{2,}$/.test(part)) {
509
- // Handle .., ..., ...., etc in path
510
772
  const levels = part.length - 1;
511
773
  for (let i = 0; i < levels; i++) {
512
774
  if (targetPath === config.storagePath)
@@ -572,120 +834,68 @@ Storage: ${config.storagePath}
572
834
  }
573
835
  if (command === 'help') {
574
836
  return wrapResult({
575
- output: `Commands:
576
- init - Initialize rlc (first time setup)
577
- ls - List all schemas, todos, and notes
578
- tree - Show directory tree structure
579
- tree -A - Show tree with files
580
- tree --depth=N - Limit tree depth (e.g., --depth=2)
581
- map - Dungeon map visualization
582
- cd <node> - Navigate into a node
583
- cd .. - Go back to parent
584
- pwd - Show current path
585
- open - Open current folder in Finder
586
- open <folder> - Open specific folder in Finder
587
- mkdir <name> - Create new folder
588
- cp <src> <dest> - Copy file or folder
589
- mv <src> <dest> - Move/rename file or folder
590
- rm <name> - Delete file
591
- rm -rf <name> - Delete folder recursively
592
- config - Show configuration
593
- config:apiKey=<key> - Set API key
594
- v, version - Show version
595
- <description> - Create schema/todo (AI generates preview)
596
- save - Save pending schema to disk
597
- cancel - Discard pending schema
598
- clean - Show items to delete in current folder
599
- clean --yes - Delete all items in current folder
600
- exit/quit - Exit the program
601
-
602
- Clipboard:
603
- ls | pbcopy - Copy output to clipboard (macOS)
604
- tree | pbcopy - Works with any command
605
- config | copy - Alternative for Windows
837
+ output: `
838
+ === ROGUELIKE CLI ===
606
839
 
607
- Workflow:
608
- 1. Type description (e.g., "todo: deploy app")
609
- 2. AI generates schema preview
610
- 3. Refine with more instructions if needed
611
- 4. Type "save" to save or "cancel" to discard
840
+ Navigation:
841
+ ls List tasks and files
842
+ tree Show task tree with status
843
+ tree -A Include files
844
+ tree --depth=N Limit tree depth
845
+ cd <task> Navigate into task
846
+ cd .., ... Go back 1 or 2 levels
847
+ pwd Show current path
848
+ open Open folder in Finder
612
849
 
613
- Examples:
850
+ Task Management:
851
+ mkdir <name> Create new task
852
+ done Mark current task as completed (recursive)
853
+ deadline <date> Set deadline (today, tomorrow, +3d, Jan 15)
854
+ boss Toggle boss/milestone status (3x XP)
855
+ block [reason] Mark task as blocked
856
+ unblock Remove blocked status
857
+ status Show current task details
614
858
 
615
- > todo opening company in delaware
859
+ File Operations:
860
+ cp <src> <dest> Copy file or folder
861
+ mv <src> <dest> Move/rename
862
+ rm <name> Delete file
863
+ rm -rf <name> Delete folder
616
864
 
617
- ┌─ TODO opening company in delaware ───────────────────────────┐
618
- │ │
619
- ├── register business name │
620
- ├── file incorporation papers │
621
- ├── get EIN number │
622
- └── Branch: legal │
623
- └── open business bank account │
624
- │ │
625
- └───────────────────────────────────────────────────────────────┘
865
+ Gamification:
866
+ stats Show XP, level, streaks
867
+ achievements Show achievement list
868
+ map Dungeon map view
869
+ map --ai AI-generated dungeon map
626
870
 
627
- > yandex cloud production infrastructure
871
+ Schema Generation:
872
+ <description> AI generates todo/schema preview
873
+ save Save pending schema
874
+ cancel Discard pending schema
628
875
 
629
- ┌─────────────────────────────────────────────────────────────┐
630
- Yandex Cloud │
631
- │ │
632
- │ ┌──────────────────┐ ┌──────────────────┐
633
- │ │ back-fastapi │ │ admin-next │ │
634
- │ │ (VM) │ │ (VM) │ │
635
- │ └────────┬─────────┘ └──────────────────┘ │
636
- │ │ │
637
- │ ├──────────────────┬─────────────────┐ │
638
- │ │ │ │ │
639
- │ ┌────────▼────────┐ ┌─────▼──────┐ ┌──────▼────────┐ │
640
- │ │ PostgreSQL │ │ Redis │ │ Cloudflare │ │
641
- │ │ (Existing DB) │ │ Cluster │ │ R2 Storage │ │
642
- │ └─────────────────┘ └────────────┘ └───────────────┘ │
643
- └─────────────────────────────────────────────────────────────┘
876
+ Utility:
877
+ init Setup wizard
878
+ config Show settings
879
+ clean --yes Clear current folder
880
+ v, version Show version
881
+ help This help
882
+ exit, quit Exit
644
883
 
645
- > architecture production redis web application
646
-
647
- ┌─ Architecture production redis web application ────────────┐
648
- │ │
649
- ├── load-balancer │
650
- ├── web-servers │
651
- │ ├── app-server-1 │
652
- │ ├── app-server-2 │
653
- │ └── app-server-3 │
654
- ├── redis │
655
- │ ├── cache-cluster │
656
- │ └── session-store │
657
- └── database │
658
- ├── postgres-primary │
659
- └── postgres-replica │
660
- │ │
661
- └───────────────────────────────────────────────────────────────┘
662
-
663
- > kubernetes cluster with clusters postgres and redis
884
+ Clipboard:
885
+ <cmd> | pbcopy Copy output (macOS)
886
+ <cmd> | clip Copy output (Windows)
664
887
 
665
- ┌─────────────────────────────────────────────────────────────┐
666
- │ Kubernetes cluster with clusters postgres │
667
- │ │
668
- │ ┌──────────────┐ ┌──────────────┐ │
669
- │ │ postgres │ │ redis │ │
670
- │ │ │ │ │ │
671
- │ │ primary-pod │ │ cache-pod-1 │ │
672
- │ │ replica-pod-1│ │ cache-pod-2 │ │
673
- │ │ replica-pod-2│ │ │ │
674
- │ └──────┬───────┘ └──────┬───────┘ │
675
- │ │ │ │
676
- │ └──────────┬───────────┘ │
677
- │ │ │
678
- │ ┌───────▼────────┐ │
679
- │ │ worker-zones │ │
680
- │ │ zone-1 │ │
681
- │ │ zone-2 │ │
682
- │ └────────────────┘ │
683
- └─────────────────────────────────────────────────────────────┘
888
+ Deadlines:
889
+ deadline today Due today
890
+ deadline tomorrow Due tomorrow
891
+ deadline +3d Due in 3 days
892
+ deadline Jan 15 Due on date
684
893
 
685
- www.rlc.rocks`
894
+ www.rlc.rocks
895
+ `.trim()
686
896
  });
687
897
  }
688
- // Save command - save pending schema/todo
898
+ // Save command
689
899
  if (command === 'save') {
690
900
  if (!exports.sessionState.pending) {
691
901
  return wrapResult({ output: 'Nothing to save. Create a schema first.' });
@@ -693,25 +903,21 @@ www.rlc.rocks`
693
903
  const pending = exports.sessionState.pending;
694
904
  const safeName = pending.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
695
905
  if (pending.format === 'tree') {
696
- // Create folder structure from tree ASCII art
697
906
  const rootPath = path.join(currentPath, safeName);
698
907
  createFoldersFromTree(rootPath, pending.content);
699
- // Clear session
700
908
  exports.sessionState.pending = null;
701
909
  exports.sessionState.history = [];
702
910
  return wrapResult({ output: `Created todo folder: ${safeName}/` });
703
911
  }
704
912
  else {
705
- // Save as .rlc.schema file
706
913
  const schemaPath = (0, nodeConfig_1.saveSchemaFile)(currentPath, pending.title, pending.content);
707
914
  const filename = path.basename(schemaPath);
708
- // Clear session
709
915
  exports.sessionState.pending = null;
710
916
  exports.sessionState.history = [];
711
917
  return wrapResult({ output: `Saved: ${filename}` });
712
918
  }
713
919
  }
714
- // Cancel command - discard pending schema
920
+ // Cancel command
715
921
  if (command === 'cancel') {
716
922
  if (!exports.sessionState.pending) {
717
923
  return wrapResult({ output: 'Nothing to cancel.' });
@@ -720,14 +926,13 @@ www.rlc.rocks`
720
926
  exports.sessionState.history = [];
721
927
  return wrapResult({ output: 'Discarded pending schema.' });
722
928
  }
723
- // Clean command - clear current directory
929
+ // Clean command
724
930
  if (command === 'clean') {
725
931
  const entries = fs.readdirSync(currentPath);
726
932
  const toDelete = entries.filter(e => !e.startsWith('.'));
727
933
  if (toDelete.length === 0) {
728
934
  return wrapResult({ output: 'Directory is already empty.' });
729
935
  }
730
- // Check for --yes flag to skip confirmation
731
936
  if (!parts.includes('--yes') && !parts.includes('-y')) {
732
937
  return wrapResult({
733
938
  output: `Will delete ${toDelete.length} items:\n${toDelete.join('\n')}\n\nRun "clean --yes" to confirm.`
@@ -739,23 +944,20 @@ www.rlc.rocks`
739
944
  }
740
945
  return wrapResult({ output: `Deleted ${toDelete.length} items.` });
741
946
  }
742
- // AI generation - store in pending, don't save immediately
947
+ // AI generation
743
948
  const fullInput = cleanInput;
744
- // Add user message to history
745
949
  exports.sessionState.history.push({ role: 'user', content: fullInput });
746
950
  const schema = await (0, claude_1.generateSchemaWithAI)(fullInput, config, signal, exports.sessionState.history);
747
951
  if (signal?.aborted) {
748
952
  return { output: 'Command cancelled.' };
749
953
  }
750
954
  if (schema) {
751
- // Store in pending
752
955
  exports.sessionState.pending = {
753
956
  title: schema.title,
754
957
  content: schema.content,
755
958
  format: schema.format,
756
959
  tree: schema.tree
757
960
  };
758
- // Add assistant response to history
759
961
  exports.sessionState.history.push({ role: 'assistant', content: schema.content });
760
962
  const safeName = schema.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
761
963
  const saveHint = schema.format === 'tree'