roguelike-cli 1.2.5 → 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,22 +601,28 @@ 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 (.., ...)
334
- if (command === '..' || command === '...') {
335
- let levels = command === '...' ? 2 : 1;
610
+ // Handle navigation without 'cd' command
611
+ if (/^\.{2,}$/.test(command)) {
612
+ const levels = command.length - 1;
336
613
  let targetPath = currentPath;
614
+ if (targetPath === config.storagePath) {
615
+ return { output: 'Already at root.' };
616
+ }
337
617
  for (let i = 0; i < levels; i++) {
338
618
  const parentPath = path.dirname(targetPath);
339
- if (parentPath === config.storagePath || parentPath.length < config.storagePath.length) {
340
- return { output: 'Already at root.' };
619
+ if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
620
+ break;
341
621
  }
342
622
  targetPath = parentPath;
623
+ if (targetPath === config.storagePath) {
624
+ break;
625
+ }
343
626
  }
344
627
  return { newPath: targetPath, output: '' };
345
628
  }
@@ -392,7 +675,6 @@ async function processCommand(input, currentPath, config, signal, rl) {
392
675
  return { output: `Moved: ${source} -> ${dest}` };
393
676
  }
394
677
  catch (error) {
395
- // If rename fails (cross-device), copy then delete
396
678
  try {
397
679
  copyRecursive(sourcePath, destPath);
398
680
  fs.rmSync(sourcePath, { recursive: true, force: true });
@@ -405,23 +687,19 @@ async function processCommand(input, currentPath, config, signal, rl) {
405
687
  }
406
688
  if (command === 'open') {
407
689
  const { exec } = require('child_process');
408
- // open or open . - open current folder in system file manager
409
690
  if (parts.length < 2 || parts[1] === '.') {
410
691
  exec(`open "${currentPath}"`);
411
692
  return { output: `Opening: ${currentPath}` };
412
693
  }
413
694
  const name = parts.slice(1).join(' ');
414
695
  const targetPath = path.join(currentPath, name);
415
- // Check if target exists
416
696
  if (fs.existsSync(targetPath)) {
417
697
  const stat = fs.statSync(targetPath);
418
698
  if (stat.isDirectory()) {
419
- // It's a folder, open in file manager
420
699
  exec(`open "${targetPath}"`);
421
700
  return { output: `Opening: ${targetPath}` };
422
701
  }
423
702
  if (stat.isFile()) {
424
- // It's a file, show its content (supports | pbcopy)
425
703
  const content = fs.readFileSync(targetPath, 'utf-8');
426
704
  return wrapResult({ output: content });
427
705
  }
@@ -468,33 +746,43 @@ async function processCommand(input, currentPath, config, signal, rl) {
468
746
  return { output: 'Usage: cd <node> or cd .. or cd <path>' };
469
747
  }
470
748
  const target = parts.slice(1).join(' ');
471
- if (target === '..') {
472
- const parentPath = path.dirname(currentPath);
473
- if (parentPath === config.storagePath || parentPath.length < config.storagePath.length) {
749
+ if (/^\.{2,}$/.test(target)) {
750
+ const levels = target.length - 1;
751
+ let targetPath = currentPath;
752
+ if (targetPath === config.storagePath) {
474
753
  return { output: 'Already at root.' };
475
754
  }
476
- return { newPath: parentPath, output: '' };
477
- }
478
- if (target === '...') {
479
- let targetPath = path.dirname(currentPath);
480
- targetPath = path.dirname(targetPath);
481
- if (targetPath.length < config.storagePath.length) {
482
- return { output: 'Already at root.' };
755
+ for (let i = 0; i < levels; i++) {
756
+ const parentPath = path.dirname(targetPath);
757
+ if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
758
+ break;
759
+ }
760
+ targetPath = parentPath;
761
+ if (targetPath === config.storagePath) {
762
+ break;
763
+ }
483
764
  }
484
765
  return { newPath: targetPath, output: '' };
485
766
  }
486
- // Handle paths like "cd bank/account" or "cd ../other"
487
767
  if (target.includes('/')) {
488
768
  let targetPath = currentPath;
489
769
  const pathParts = target.split('/');
490
770
  for (const part of pathParts) {
491
- if (part === '..') {
492
- targetPath = path.dirname(targetPath);
771
+ if (/^\.{2,}$/.test(part)) {
772
+ const levels = part.length - 1;
773
+ for (let i = 0; i < levels; i++) {
774
+ if (targetPath === config.storagePath)
775
+ break;
776
+ const parentPath = path.dirname(targetPath);
777
+ if (parentPath.length < config.storagePath.length)
778
+ break;
779
+ targetPath = parentPath;
780
+ }
493
781
  }
494
782
  else if (part === '.') {
495
783
  continue;
496
784
  }
497
- else {
785
+ else if (part) {
498
786
  const newPath = (0, storage_1.navigateToNode)(targetPath, part);
499
787
  if (!newPath) {
500
788
  return { output: `Path "${target}" not found.` };
@@ -546,120 +834,68 @@ Storage: ${config.storagePath}
546
834
  }
547
835
  if (command === 'help') {
548
836
  return wrapResult({
549
- output: `Commands:
550
- init - Initialize rlc (first time setup)
551
- ls - List all schemas, todos, and notes
552
- tree - Show directory tree structure
553
- tree -A - Show tree with files
554
- tree --depth=N - Limit tree depth (e.g., --depth=2)
555
- map - Dungeon map visualization
556
- cd <node> - Navigate into a node
557
- cd .. - Go back to parent
558
- pwd - Show current path
559
- open - Open current folder in Finder
560
- open <folder> - Open specific folder in Finder
561
- mkdir <name> - Create new folder
562
- cp <src> <dest> - Copy file or folder
563
- mv <src> <dest> - Move/rename file or folder
564
- rm <name> - Delete file
565
- rm -rf <name> - Delete folder recursively
566
- config - Show configuration
567
- config:apiKey=<key> - Set API key
568
- v, version - Show version
569
- <description> - Create schema/todo (AI generates preview)
570
- save - Save pending schema to disk
571
- cancel - Discard pending schema
572
- clean - Show items to delete in current folder
573
- clean --yes - Delete all items in current folder
574
- exit/quit - Exit the program
575
-
576
- Clipboard:
577
- ls | pbcopy - Copy output to clipboard (macOS)
578
- tree | pbcopy - Works with any command
579
- config | copy - Alternative for Windows
837
+ output: `
838
+ === ROGUELIKE CLI ===
580
839
 
581
- Workflow:
582
- 1. Type description (e.g., "todo: deploy app")
583
- 2. AI generates schema preview
584
- 3. Refine with more instructions if needed
585
- 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
586
849
 
587
- 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
588
858
 
589
- > 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
590
864
 
591
- ┌─ TODO opening company in delaware ───────────────────────────┐
592
- │ │
593
- ├── register business name │
594
- ├── file incorporation papers │
595
- ├── get EIN number │
596
- └── Branch: legal │
597
- └── open business bank account │
598
- │ │
599
- └───────────────────────────────────────────────────────────────┘
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
600
870
 
601
- > yandex cloud production infrastructure
871
+ Schema Generation:
872
+ <description> AI generates todo/schema preview
873
+ save Save pending schema
874
+ cancel Discard pending schema
602
875
 
603
- ┌─────────────────────────────────────────────────────────────┐
604
- Yandex Cloud │
605
- │ │
606
- │ ┌──────────────────┐ ┌──────────────────┐
607
- │ │ back-fastapi │ │ admin-next │ │
608
- │ │ (VM) │ │ (VM) │ │
609
- │ └────────┬─────────┘ └──────────────────┘ │
610
- │ │ │
611
- │ ├──────────────────┬─────────────────┐ │
612
- │ │ │ │ │
613
- │ ┌────────▼────────┐ ┌─────▼──────┐ ┌──────▼────────┐ │
614
- │ │ PostgreSQL │ │ Redis │ │ Cloudflare │ │
615
- │ │ (Existing DB) │ │ Cluster │ │ R2 Storage │ │
616
- │ └─────────────────┘ └────────────┘ └───────────────┘ │
617
- └─────────────────────────────────────────────────────────────┘
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
618
883
 
619
- > architecture production redis web application
620
-
621
- ┌─ Architecture production redis web application ────────────┐
622
- │ │
623
- ├── load-balancer │
624
- ├── web-servers │
625
- │ ├── app-server-1 │
626
- │ ├── app-server-2 │
627
- │ └── app-server-3 │
628
- ├── redis │
629
- │ ├── cache-cluster │
630
- │ └── session-store │
631
- └── database │
632
- ├── postgres-primary │
633
- └── postgres-replica │
634
- │ │
635
- └───────────────────────────────────────────────────────────────┘
636
-
637
- > kubernetes cluster with clusters postgres and redis
884
+ Clipboard:
885
+ <cmd> | pbcopy Copy output (macOS)
886
+ <cmd> | clip Copy output (Windows)
638
887
 
639
- ┌─────────────────────────────────────────────────────────────┐
640
- │ Kubernetes cluster with clusters postgres │
641
- │ │
642
- │ ┌──────────────┐ ┌──────────────┐ │
643
- │ │ postgres │ │ redis │ │
644
- │ │ │ │ │ │
645
- │ │ primary-pod │ │ cache-pod-1 │ │
646
- │ │ replica-pod-1│ │ cache-pod-2 │ │
647
- │ │ replica-pod-2│ │ │ │
648
- │ └──────┬───────┘ └──────┬───────┘ │
649
- │ │ │ │
650
- │ └──────────┬───────────┘ │
651
- │ │ │
652
- │ ┌───────▼────────┐ │
653
- │ │ worker-zones │ │
654
- │ │ zone-1 │ │
655
- │ │ zone-2 │ │
656
- │ └────────────────┘ │
657
- └─────────────────────────────────────────────────────────────┘
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
658
893
 
659
- www.rlc.rocks`
894
+ www.rlc.rocks
895
+ `.trim()
660
896
  });
661
897
  }
662
- // Save command - save pending schema/todo
898
+ // Save command
663
899
  if (command === 'save') {
664
900
  if (!exports.sessionState.pending) {
665
901
  return wrapResult({ output: 'Nothing to save. Create a schema first.' });
@@ -667,25 +903,21 @@ www.rlc.rocks`
667
903
  const pending = exports.sessionState.pending;
668
904
  const safeName = pending.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
669
905
  if (pending.format === 'tree') {
670
- // Create folder structure from tree ASCII art
671
906
  const rootPath = path.join(currentPath, safeName);
672
907
  createFoldersFromTree(rootPath, pending.content);
673
- // Clear session
674
908
  exports.sessionState.pending = null;
675
909
  exports.sessionState.history = [];
676
910
  return wrapResult({ output: `Created todo folder: ${safeName}/` });
677
911
  }
678
912
  else {
679
- // Save as .rlc.schema file
680
913
  const schemaPath = (0, nodeConfig_1.saveSchemaFile)(currentPath, pending.title, pending.content);
681
914
  const filename = path.basename(schemaPath);
682
- // Clear session
683
915
  exports.sessionState.pending = null;
684
916
  exports.sessionState.history = [];
685
917
  return wrapResult({ output: `Saved: ${filename}` });
686
918
  }
687
919
  }
688
- // Cancel command - discard pending schema
920
+ // Cancel command
689
921
  if (command === 'cancel') {
690
922
  if (!exports.sessionState.pending) {
691
923
  return wrapResult({ output: 'Nothing to cancel.' });
@@ -694,14 +926,13 @@ www.rlc.rocks`
694
926
  exports.sessionState.history = [];
695
927
  return wrapResult({ output: 'Discarded pending schema.' });
696
928
  }
697
- // Clean command - clear current directory
929
+ // Clean command
698
930
  if (command === 'clean') {
699
931
  const entries = fs.readdirSync(currentPath);
700
932
  const toDelete = entries.filter(e => !e.startsWith('.'));
701
933
  if (toDelete.length === 0) {
702
934
  return wrapResult({ output: 'Directory is already empty.' });
703
935
  }
704
- // Check for --yes flag to skip confirmation
705
936
  if (!parts.includes('--yes') && !parts.includes('-y')) {
706
937
  return wrapResult({
707
938
  output: `Will delete ${toDelete.length} items:\n${toDelete.join('\n')}\n\nRun "clean --yes" to confirm.`
@@ -713,23 +944,20 @@ www.rlc.rocks`
713
944
  }
714
945
  return wrapResult({ output: `Deleted ${toDelete.length} items.` });
715
946
  }
716
- // AI generation - store in pending, don't save immediately
947
+ // AI generation
717
948
  const fullInput = cleanInput;
718
- // Add user message to history
719
949
  exports.sessionState.history.push({ role: 'user', content: fullInput });
720
950
  const schema = await (0, claude_1.generateSchemaWithAI)(fullInput, config, signal, exports.sessionState.history);
721
951
  if (signal?.aborted) {
722
952
  return { output: 'Command cancelled.' };
723
953
  }
724
954
  if (schema) {
725
- // Store in pending
726
955
  exports.sessionState.pending = {
727
956
  title: schema.title,
728
957
  content: schema.content,
729
958
  format: schema.format,
730
959
  tree: schema.tree
731
960
  };
732
- // Add assistant response to history
733
961
  exports.sessionState.history.push({ role: 'assistant', content: schema.content });
734
962
  const safeName = schema.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
735
963
  const saveHint = schema.format === 'tree'