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.
@@ -4,40 +4,38 @@ import * as readline from 'readline';
4
4
  import { execSync } from 'child_process';
5
5
  import { Config } from '../config/config';
6
6
  import { listSchemas, navigateToNode, getTree } from '../storage/storage';
7
- import { saveSchemaFile, writeNodeConfig } from '../storage/nodeConfig';
8
- import { generateSchemaWithAI } from '../ai/claude';
7
+ import { saveSchemaFile, writeNodeConfig, readNodeConfig, calculateXP, saveMapFile, NodeConfig } from '../storage/nodeConfig';
8
+ import { generateSchemaWithAI, generateDungeonMapWithAI } from '../ai/claude';
9
+ import { completeTask, formatStats, formatAchievements, readProfile } from '../storage/profile';
9
10
 
10
11
  // Parse tree ASCII art and create folder structure
11
12
  function createFoldersFromTree(rootPath: string, treeContent: string): void {
12
- // Create root folder
13
13
  if (!fs.existsSync(rootPath)) {
14
14
  fs.mkdirSync(rootPath, { recursive: true });
15
15
  }
16
16
 
17
- // Parse tree lines
18
17
  const lines = treeContent.split('\n');
19
18
  const stack: { path: string; indent: number }[] = [{ path: rootPath, indent: -1 }];
20
19
 
21
20
  for (const line of lines) {
22
- // Skip empty lines
23
21
  if (!line.trim()) continue;
24
22
 
25
- // Extract node name from tree line
26
- // Patterns: "├── Name", "└── Name", "│ ├── Name", etc.
27
23
  const match = line.match(/^([\s│]*)[├└]──\s*(.+)$/);
28
24
  if (!match) continue;
29
25
 
30
26
  const prefix = match[1];
31
27
  let nodeName = match[2].trim();
32
-
33
- // Calculate indent level (each │ or space block = 1 level)
34
28
  const indent = Math.floor(prefix.replace(/│/g, ' ').length / 4);
35
29
 
36
- // Clean node name - remove extra info in parentheses, brackets
30
+ // Extract metadata from node name
31
+ const isBoss = /\[BOSS\]/i.test(nodeName) || /\[MILESTONE\]/i.test(nodeName);
32
+ const deadlineMatch = nodeName.match(/\[(?:DUE|DEADLINE):\s*([^\]]+)\]/i);
33
+ const deadline = deadlineMatch ? deadlineMatch[1].trim() : undefined;
34
+
35
+ // Clean node name
37
36
  nodeName = nodeName.replace(/\s*\([^)]*\)\s*/g, '').trim();
38
37
  nodeName = nodeName.replace(/\s*\[[^\]]*\]\s*/g, '').trim();
39
38
 
40
- // Create safe folder name
41
39
  const safeName = nodeName
42
40
  .toLowerCase()
43
41
  .replace(/[^a-z0-9]+/g, '-')
@@ -45,7 +43,6 @@ function createFoldersFromTree(rootPath: string, treeContent: string): void {
45
43
 
46
44
  if (!safeName) continue;
47
45
 
48
- // Pop stack until we find parent
49
46
  while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
50
47
  stack.pop();
51
48
  }
@@ -53,25 +50,29 @@ function createFoldersFromTree(rootPath: string, treeContent: string): void {
53
50
  const parentPath = stack[stack.length - 1].path;
54
51
  const folderPath = path.join(parentPath, safeName);
55
52
 
56
- // Create folder
57
53
  if (!fs.existsSync(folderPath)) {
58
54
  fs.mkdirSync(folderPath, { recursive: true });
59
55
  }
60
56
 
61
- // Create node config
57
+ // Calculate depth for XP
58
+ const depth = stack.length;
59
+
62
60
  writeNodeConfig(folderPath, {
63
61
  name: nodeName,
62
+ status: 'open',
63
+ xp: calculateXP(depth, isBoss),
64
+ isBoss,
65
+ deadline,
64
66
  createdAt: new Date().toISOString(),
65
67
  updatedAt: new Date().toISOString(),
66
68
  });
67
69
 
68
- // Push to stack
69
70
  stack.push({ path: folderPath, indent });
70
71
  }
71
72
  }
72
73
 
73
74
  // Generate dungeon map visualization from folder structure
74
- function generateDungeonMap(dirPath: string): string {
75
+ function generateDungeonMap(dirPath: string, config: Config): string {
75
76
  if (!fs.existsSync(dirPath)) {
76
77
  return 'Directory does not exist.';
77
78
  }
@@ -86,32 +87,33 @@ function generateDungeonMap(dirPath: string): string {
86
87
  const lines: string[] = [];
87
88
  const roomWidth = 20;
88
89
  const roomsPerRow = 2;
89
- const wall = '';
90
+ const wall = '#';
90
91
  const door = '+';
91
92
  const task = '*';
92
93
  const milestone = '@';
94
+ const done = 'x';
95
+ const blocked = '!';
93
96
 
94
- // Group folders into rows of 2
95
97
  const rows: typeof folders[] = [];
96
98
  for (let i = 0; i < folders.length; i += roomsPerRow) {
97
99
  rows.push(folders.slice(i, i + roomsPerRow));
98
100
  }
99
101
 
100
- // Top border
102
+ lines.push('');
101
103
  lines.push(' ' + wall.repeat(roomWidth * roomsPerRow + 3));
102
104
 
103
105
  rows.forEach((row, rowIndex) => {
104
- // Room content
105
106
  for (let line = 0; line < 6; line++) {
106
107
  let rowStr = ' ' + wall;
107
108
 
108
109
  row.forEach((folder, colIndex) => {
109
- const name = folder.name.replace(/-/g, ' ');
110
+ const folderPath = path.join(dirPath, folder.name);
111
+ const nodeConfig = readNodeConfig(folderPath);
112
+ const name = (nodeConfig?.name || folder.name).replace(/-/g, ' ');
110
113
  const displayName = name.length > roomWidth - 4
111
114
  ? name.substring(0, roomWidth - 7) + '...'
112
115
  : name;
113
116
 
114
- // Get sub-items
115
117
  const subPath = path.join(dirPath, folder.name);
116
118
  const subEntries = fs.existsSync(subPath)
117
119
  ? fs.readdirSync(subPath, { withFileTypes: true })
@@ -119,25 +121,32 @@ function generateDungeonMap(dirPath: string): string {
119
121
  : [];
120
122
 
121
123
  if (line === 0) {
122
- // Empty line
123
124
  rowStr += ' '.repeat(roomWidth - 1) + wall;
124
125
  } else if (line === 1) {
125
- // Room name
126
+ const statusIcon = nodeConfig?.status === 'done' ? '[DONE]'
127
+ : nodeConfig?.isBoss ? '[BOSS]'
128
+ : '';
129
+ const title = statusIcon ? `${statusIcon}` : `[${displayName}]`;
130
+ const padding = roomWidth - title.length - 1;
131
+ rowStr += ' ' + title + ' '.repeat(Math.max(0, padding - 1)) + wall;
132
+ } else if (line === 2 && !nodeConfig?.isBoss) {
126
133
  const title = `[${displayName}]`;
127
134
  const padding = roomWidth - title.length - 1;
128
135
  rowStr += ' ' + title + ' '.repeat(Math.max(0, padding - 1)) + wall;
129
136
  } else if (line >= 2 && line <= 4) {
130
- // Sub-items
131
- const itemIndex = line - 2;
132
- if (itemIndex < subEntries.length) {
133
- const subName = subEntries[itemIndex].name.replace(/-/g, ' ');
137
+ const itemIndex = nodeConfig?.isBoss ? line - 2 : line - 3;
138
+ if (itemIndex >= 0 && itemIndex < subEntries.length) {
139
+ const subConfig = readNodeConfig(path.join(subPath, subEntries[itemIndex].name));
140
+ const subName = (subConfig?.name || subEntries[itemIndex].name).replace(/-/g, ' ');
134
141
  const shortName = subName.length > roomWidth - 6
135
142
  ? subName.substring(0, roomWidth - 9) + '...'
136
143
  : subName;
137
- const marker = subName.toLowerCase().includes('boss') ||
138
- subName.toLowerCase().includes('launch') ||
139
- subName.toLowerCase().includes('deploy')
140
- ? milestone : task;
144
+
145
+ let marker = task;
146
+ if (subConfig?.status === 'done') marker = done;
147
+ else if (subConfig?.status === 'blocked') marker = blocked;
148
+ else if (subConfig?.isBoss) marker = milestone;
149
+
141
150
  const itemStr = `${marker} ${shortName}`;
142
151
  const itemPadding = roomWidth - itemStr.length - 1;
143
152
  rowStr += ' ' + itemStr + ' '.repeat(Math.max(0, itemPadding - 1)) + wall;
@@ -145,11 +154,9 @@ function generateDungeonMap(dirPath: string): string {
145
154
  rowStr += ' '.repeat(roomWidth - 1) + wall;
146
155
  }
147
156
  } else {
148
- // Empty line
149
157
  rowStr += ' '.repeat(roomWidth - 1) + wall;
150
158
  }
151
159
 
152
- // Add door between rooms
153
160
  if (colIndex < row.length - 1 && line === 3) {
154
161
  rowStr = rowStr.slice(0, -1) + door + door + door;
155
162
  } else if (colIndex < row.length - 1) {
@@ -157,7 +164,6 @@ function generateDungeonMap(dirPath: string): string {
157
164
  }
158
165
  });
159
166
 
160
- // Fill empty space if odd number of rooms
161
167
  if (row.length < roomsPerRow) {
162
168
  rowStr += ' '.repeat(roomWidth) + wall;
163
169
  }
@@ -165,16 +171,14 @@ function generateDungeonMap(dirPath: string): string {
165
171
  lines.push(rowStr);
166
172
  }
167
173
 
168
- // Bottom border with doors to next row
169
174
  if (rowIndex < rows.length - 1) {
170
175
  let borderStr = ' ' + wall.repeat(Math.floor(roomWidth / 2)) + door;
171
176
  borderStr += wall.repeat(roomWidth - 1) + door;
172
177
  borderStr += wall.repeat(Math.floor(roomWidth / 2) + 1);
173
178
  lines.push(borderStr);
174
179
 
175
- // Corridor
176
- let corridorStr = ' ' + ' '.repeat(Math.floor(roomWidth / 2)) + '';
177
- corridorStr += ' '.repeat(roomWidth - 1) + '│';
180
+ let corridorStr = ' ' + ' '.repeat(Math.floor(roomWidth / 2)) + '|';
181
+ corridorStr += ' '.repeat(roomWidth - 1) + '|';
178
182
  lines.push(corridorStr);
179
183
 
180
184
  borderStr = ' ' + wall.repeat(Math.floor(roomWidth / 2)) + door;
@@ -184,23 +188,111 @@ function generateDungeonMap(dirPath: string): string {
184
188
  }
185
189
  });
186
190
 
187
- // Bottom border
188
191
  lines.push(' ' + wall.repeat(roomWidth * roomsPerRow + 3));
189
192
 
190
- // Legend
191
193
  lines.push('');
192
- lines.push(`Legend: ${task} Task ${milestone} Milestone ${door} Door ${wall} Wall`);
194
+ lines.push(`Legend: ${task} Task ${done} Done ${milestone} Boss/Milestone ${blocked} Blocked ${door} Door`);
193
195
 
194
196
  return lines.join('\n');
195
197
  }
196
198
 
199
+ // Parse human-readable date
200
+ function parseDeadline(input: string): string | null {
201
+ const lower = input.toLowerCase().trim();
202
+ const today = new Date();
203
+
204
+ if (lower === 'today') {
205
+ return today.toISOString().split('T')[0];
206
+ }
207
+
208
+ if (lower === 'tomorrow') {
209
+ today.setDate(today.getDate() + 1);
210
+ return today.toISOString().split('T')[0];
211
+ }
212
+
213
+ // +Nd format (e.g., +3d, +7d)
214
+ const plusDaysMatch = lower.match(/^\+(\d+)d$/);
215
+ if (plusDaysMatch) {
216
+ today.setDate(today.getDate() + parseInt(plusDaysMatch[1]));
217
+ return today.toISOString().split('T')[0];
218
+ }
219
+
220
+ // Try parsing as date
221
+ const parsed = new Date(input);
222
+ if (!isNaN(parsed.getTime())) {
223
+ return parsed.toISOString().split('T')[0];
224
+ }
225
+
226
+ return null;
227
+ }
228
+
229
+ // Format deadline for display
230
+ function formatDeadline(deadline: string): string {
231
+ const deadlineDate = new Date(deadline);
232
+ const today = new Date();
233
+ today.setHours(0, 0, 0, 0);
234
+ deadlineDate.setHours(0, 0, 0, 0);
235
+
236
+ const diffDays = Math.ceil((deadlineDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
237
+
238
+ if (diffDays < 0) return `OVERDUE ${Math.abs(diffDays)}d`;
239
+ if (diffDays === 0) return 'TODAY';
240
+ if (diffDays === 1) return 'tomorrow';
241
+ if (diffDays <= 7) return `${diffDays}d left`;
242
+
243
+ return deadlineDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
244
+ }
245
+
246
+ // Get depth of current path relative to storage root
247
+ function getDepth(currentPath: string, storagePath: string): number {
248
+ const relative = path.relative(storagePath, currentPath);
249
+ if (!relative) return 0;
250
+ return relative.split(path.sep).length;
251
+ }
252
+
253
+ // Mark node as done recursively
254
+ function markDoneRecursive(nodePath: string, storagePath: string): { xpGained: number; tasksCompleted: number; bossesDefeated: number } {
255
+ let result = { xpGained: 0, tasksCompleted: 0, bossesDefeated: 0 };
256
+
257
+ const config = readNodeConfig(nodePath);
258
+ if (!config || config.status === 'done') {
259
+ return result;
260
+ }
261
+
262
+ // First, mark all children as done
263
+ const entries = fs.readdirSync(nodePath, { withFileTypes: true });
264
+ for (const entry of entries) {
265
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
266
+ const childResult = markDoneRecursive(path.join(nodePath, entry.name), storagePath);
267
+ result.xpGained += childResult.xpGained;
268
+ result.tasksCompleted += childResult.tasksCompleted;
269
+ result.bossesDefeated += childResult.bossesDefeated;
270
+ }
271
+ }
272
+
273
+ // Mark this node as done
274
+ const depth = getDepth(nodePath, storagePath);
275
+ const xp = config.xp || calculateXP(depth, config.isBoss || false);
276
+
277
+ writeNodeConfig(nodePath, {
278
+ ...config,
279
+ status: 'done',
280
+ completedAt: new Date().toISOString(),
281
+ });
282
+
283
+ result.xpGained += xp;
284
+ result.tasksCompleted += 1;
285
+ if (config.isBoss) result.bossesDefeated += 1;
286
+
287
+ return result;
288
+ }
289
+
197
290
  export interface CommandResult {
198
291
  output?: string;
199
292
  newPath?: string;
200
293
  reloadConfig?: boolean;
201
294
  }
202
295
 
203
- // Pending schema waiting to be saved
204
296
  export interface PendingSchema {
205
297
  title: string;
206
298
  content: string;
@@ -208,25 +300,21 @@ export interface PendingSchema {
208
300
  tree?: any[];
209
301
  }
210
302
 
211
- // Conversation history for AI context
212
303
  export interface ConversationMessage {
213
304
  role: 'user' | 'assistant';
214
305
  content: string;
215
306
  }
216
307
 
217
- // Session state for dialog mode
218
308
  export interface SessionState {
219
309
  pending: PendingSchema | null;
220
310
  history: ConversationMessage[];
221
311
  }
222
312
 
223
- // Global session state
224
313
  export const sessionState: SessionState = {
225
314
  pending: null,
226
315
  history: []
227
316
  };
228
317
 
229
- // Format items in columns like native ls
230
318
  function formatColumns(items: string[], termWidth: number = 80): string {
231
319
  if (items.length === 0) return '';
232
320
 
@@ -242,7 +330,6 @@ function formatColumns(items: string[], termWidth: number = 80): string {
242
330
  return rows.join('\n');
243
331
  }
244
332
 
245
- // Copy to clipboard (cross-platform)
246
333
  function copyToClipboard(text: string): void {
247
334
  const platform = process.platform;
248
335
  try {
@@ -251,7 +338,6 @@ function copyToClipboard(text: string): void {
251
338
  } else if (platform === 'win32') {
252
339
  execSync('clip', { input: text });
253
340
  } else {
254
- // Linux - try xclip or xsel
255
341
  try {
256
342
  execSync('xclip -selection clipboard', { input: text });
257
343
  } catch {
@@ -259,11 +345,10 @@ function copyToClipboard(text: string): void {
259
345
  }
260
346
  }
261
347
  } catch (e) {
262
- // Silently fail if clipboard not available
348
+ // Silently fail
263
349
  }
264
350
  }
265
351
 
266
- // Helper function for recursive copy
267
352
  function copyRecursive(src: string, dest: string): void {
268
353
  const stat = fs.statSync(src);
269
354
 
@@ -283,6 +368,81 @@ function copyRecursive(src: string, dest: string): void {
283
368
  }
284
369
  }
285
370
 
371
+ // Build tree with status and deadline info
372
+ function getTreeWithStatus(
373
+ dirPath: string,
374
+ prefix: string = '',
375
+ isRoot: boolean = true,
376
+ maxDepth: number = 10,
377
+ currentDepth: number = 0,
378
+ showFiles: boolean = false
379
+ ): string[] {
380
+ const lines: string[] = [];
381
+
382
+ if (!fs.existsSync(dirPath)) {
383
+ return lines;
384
+ }
385
+
386
+ if (currentDepth >= maxDepth) {
387
+ return lines;
388
+ }
389
+
390
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
391
+ const dirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.'));
392
+ const files = showFiles ? entries.filter(e => e.isFile() && !e.name.startsWith('.')) : [];
393
+
394
+ const allItems = [...dirs, ...files];
395
+
396
+ allItems.forEach((entry, index) => {
397
+ const isLast = index === allItems.length - 1;
398
+ const connector = isLast ? '└── ' : '├── ';
399
+ const nextPrefix = isLast ? ' ' : '│ ';
400
+
401
+ if (entry.isDirectory()) {
402
+ const nodePath = path.join(dirPath, entry.name);
403
+ const config = readNodeConfig(nodePath);
404
+
405
+ let displayName = config?.name || entry.name;
406
+ const tags: string[] = [];
407
+
408
+ // Add status tag
409
+ if (config?.status === 'done') {
410
+ tags.push('DONE');
411
+ } else if (config?.status === 'blocked') {
412
+ tags.push('BLOCKED');
413
+ }
414
+
415
+ // Add boss tag
416
+ if (config?.isBoss) {
417
+ tags.push('BOSS');
418
+ }
419
+
420
+ // Add deadline tag
421
+ if (config?.deadline && config.status !== 'done') {
422
+ tags.push(formatDeadline(config.deadline));
423
+ }
424
+
425
+ const tagStr = tags.length > 0 ? ` [${tags.join('] [')}]` : '';
426
+
427
+ lines.push(`${prefix}${connector}${displayName}/${tagStr}`);
428
+
429
+ const childLines = getTreeWithStatus(
430
+ nodePath,
431
+ prefix + nextPrefix,
432
+ false,
433
+ maxDepth,
434
+ currentDepth + 1,
435
+ showFiles
436
+ );
437
+ lines.push(...childLines);
438
+ } else {
439
+ lines.push(`${prefix}${connector}${entry.name}`);
440
+ }
441
+ });
442
+
443
+ return lines;
444
+ }
445
+
286
446
  export async function processCommand(
287
447
  input: string,
288
448
  currentPath: string,
@@ -290,7 +450,6 @@ export async function processCommand(
290
450
  signal?: AbortSignal,
291
451
  rl?: readline.Interface
292
452
  ): Promise<CommandResult> {
293
- // Check for clipboard pipe
294
453
  const clipboardPipe = /\s*\|\s*(pbcopy|copy|clip)\s*$/i;
295
454
  const shouldCopy = clipboardPipe.test(input);
296
455
  const cleanInput = input.replace(clipboardPipe, '').trim();
@@ -298,7 +457,6 @@ export async function processCommand(
298
457
  const parts = cleanInput.split(' ').filter(p => p.length > 0);
299
458
  const command = parts[0].toLowerCase();
300
459
 
301
- // Helper to wrap result with clipboard copy
302
460
  const wrapResult = (result: CommandResult): CommandResult => {
303
461
  if (shouldCopy && result.output) {
304
462
  copyToClipboard(result.output);
@@ -313,9 +471,202 @@ export async function processCommand(
313
471
  return wrapResult({ output: `Roguelike CLI v${pkg.version}` });
314
472
  }
315
473
 
474
+ // Stats command
475
+ if (command === 'stats') {
476
+ return wrapResult({ output: formatStats() });
477
+ }
478
+
479
+ // Achievements command
480
+ if (command === 'achievements' || command === 'ach') {
481
+ return wrapResult({ output: formatAchievements() });
482
+ }
483
+
484
+ // Done command - mark current node as completed
485
+ if (command === 'done') {
486
+ const nodeConfig = readNodeConfig(currentPath);
487
+
488
+ if (!nodeConfig) {
489
+ return wrapResult({ output: 'No task at current location. Navigate to a task first.' });
490
+ }
491
+
492
+ if (nodeConfig.status === 'done') {
493
+ return wrapResult({ output: 'This task is already completed.' });
494
+ }
495
+
496
+ // Mark done recursively
497
+ const result = markDoneRecursive(currentPath, config.storagePath);
498
+
499
+ // Update profile with XP and achievements
500
+ const depth = getDepth(currentPath, config.storagePath);
501
+ const taskResult = completeTask(
502
+ result.xpGained,
503
+ nodeConfig.isBoss || false,
504
+ depth,
505
+ nodeConfig.createdAt
506
+ );
507
+
508
+ let output = `\n=== TASK COMPLETED ===\n`;
509
+ output += `\nTasks completed: ${result.tasksCompleted}`;
510
+ if (result.bossesDefeated > 0) {
511
+ output += `\nBosses defeated: ${result.bossesDefeated}`;
512
+ }
513
+ output += `\n+${result.xpGained} XP`;
514
+
515
+ if (taskResult.levelUp) {
516
+ output += `\n\n*** LEVEL UP! ***`;
517
+ output += `\nYou are now level ${taskResult.newLevel}!`;
518
+ }
519
+
520
+ if (taskResult.newAchievements.length > 0) {
521
+ output += `\n\n=== NEW ACHIEVEMENTS ===`;
522
+ for (const ach of taskResult.newAchievements) {
523
+ output += `\n[x] ${ach.name}: ${ach.description}`;
524
+ }
525
+ }
526
+
527
+ output += '\n';
528
+
529
+ return wrapResult({ output });
530
+ }
531
+
532
+ // Deadline command
533
+ if (command === 'deadline') {
534
+ if (parts.length < 2) {
535
+ return wrapResult({ output: 'Usage: deadline <date>\nExamples: deadline today, deadline tomorrow, deadline +3d, deadline Jan 15' });
536
+ }
537
+
538
+ const dateStr = parts.slice(1).join(' ');
539
+ const deadline = parseDeadline(dateStr);
540
+
541
+ if (!deadline) {
542
+ return wrapResult({ output: `Could not parse date: ${dateStr}` });
543
+ }
544
+
545
+ const nodeConfig = readNodeConfig(currentPath);
546
+ if (!nodeConfig) {
547
+ return wrapResult({ output: 'No task at current location.' });
548
+ }
549
+
550
+ writeNodeConfig(currentPath, {
551
+ ...nodeConfig,
552
+ deadline,
553
+ });
554
+
555
+ return wrapResult({ output: `Deadline set: ${formatDeadline(deadline)}` });
556
+ }
557
+
558
+ // Boss command - mark as boss/milestone
559
+ if (command === 'boss' || command === 'milestone') {
560
+ const nodeConfig = readNodeConfig(currentPath);
561
+ if (!nodeConfig) {
562
+ return wrapResult({ output: 'No task at current location.' });
563
+ }
564
+
565
+ const newIsBoss = !nodeConfig.isBoss;
566
+ const depth = getDepth(currentPath, config.storagePath);
567
+
568
+ writeNodeConfig(currentPath, {
569
+ ...nodeConfig,
570
+ isBoss: newIsBoss,
571
+ xp: calculateXP(depth, newIsBoss),
572
+ });
573
+
574
+ return wrapResult({ output: newIsBoss ? 'Marked as BOSS task (3x XP)' : 'Removed BOSS status' });
575
+ }
576
+
577
+ // Block command
578
+ if (command === 'block') {
579
+ const nodeConfig = readNodeConfig(currentPath);
580
+ if (!nodeConfig) {
581
+ return wrapResult({ output: 'No task at current location.' });
582
+ }
583
+
584
+ const reason = parts.length > 1 ? parts.slice(1).join(' ') : undefined;
585
+
586
+ writeNodeConfig(currentPath, {
587
+ ...nodeConfig,
588
+ status: 'blocked',
589
+ blockedBy: reason ? [reason] : nodeConfig.blockedBy,
590
+ });
591
+
592
+ return wrapResult({ output: reason ? `Blocked: ${reason}` : 'Task marked as blocked' });
593
+ }
594
+
595
+ // Unblock command
596
+ if (command === 'unblock') {
597
+ const nodeConfig = readNodeConfig(currentPath);
598
+ if (!nodeConfig) {
599
+ return wrapResult({ output: 'No task at current location.' });
600
+ }
601
+
602
+ writeNodeConfig(currentPath, {
603
+ ...nodeConfig,
604
+ status: 'open',
605
+ blockedBy: [],
606
+ });
607
+
608
+ return wrapResult({ output: 'Task unblocked' });
609
+ }
610
+
611
+ // Status command - show current task status
612
+ if (command === 'status') {
613
+ const nodeConfig = readNodeConfig(currentPath);
614
+ if (!nodeConfig) {
615
+ return wrapResult({ output: 'No task at current location.' });
616
+ }
617
+
618
+ const lines: string[] = [
619
+ '',
620
+ `Task: ${nodeConfig.name}`,
621
+ `Status: ${nodeConfig.status.toUpperCase()}`,
622
+ `XP: ${nodeConfig.xp}`,
623
+ ];
624
+
625
+ if (nodeConfig.isBoss) {
626
+ lines.push('Type: BOSS');
627
+ }
628
+
629
+ if (nodeConfig.deadline) {
630
+ lines.push(`Deadline: ${formatDeadline(nodeConfig.deadline)}`);
631
+ }
632
+
633
+ if (nodeConfig.completedAt) {
634
+ lines.push(`Completed: ${new Date(nodeConfig.completedAt).toLocaleDateString()}`);
635
+ }
636
+
637
+ if (nodeConfig.blockedBy && nodeConfig.blockedBy.length > 0) {
638
+ lines.push(`Blocked by: ${nodeConfig.blockedBy.join(', ')}`);
639
+ }
640
+
641
+ lines.push('');
642
+
643
+ return wrapResult({ output: lines.join('\n') });
644
+ }
645
+
316
646
  // Map command - dungeon visualization
317
647
  if (command === 'map') {
318
- const dungeonMap = generateDungeonMap(currentPath);
648
+ // Check for --ai flag to use AI generation
649
+ if (parts.includes('--ai') || parts.includes('-a')) {
650
+ const treeLines = getTreeWithStatus(currentPath, '', true, 10, 0, false);
651
+ const treeContent = treeLines.join('\n');
652
+
653
+ if (!treeContent) {
654
+ return wrapResult({ output: 'No tasks to visualize.' });
655
+ }
656
+
657
+ const mapContent = await generateDungeonMapWithAI(treeContent, config, signal);
658
+
659
+ if (mapContent) {
660
+ // Save map to file
661
+ const folderName = path.basename(currentPath);
662
+ saveMapFile(currentPath, folderName + '-map', mapContent);
663
+ return wrapResult({ output: mapContent + '\n\n[Map saved as .rlc.map]' });
664
+ }
665
+
666
+ return wrapResult({ output: 'Could not generate AI map. Using default.' });
667
+ }
668
+
669
+ const dungeonMap = generateDungeonMap(currentPath, config);
319
670
  return wrapResult({ output: dungeonMap });
320
671
  }
321
672
 
@@ -331,7 +682,15 @@ export async function processCommand(
331
682
  if (entry.name.startsWith('.')) continue;
332
683
 
333
684
  if (entry.isDirectory()) {
334
- items.push(entry.name + '/');
685
+ const nodePath = path.join(currentPath, entry.name);
686
+ const config = readNodeConfig(nodePath);
687
+ let suffix = '/';
688
+
689
+ if (config?.status === 'done') suffix = '/ [DONE]';
690
+ else if (config?.status === 'blocked') suffix = '/ [BLOCKED]';
691
+ else if (config?.isBoss) suffix = '/ [BOSS]';
692
+
693
+ items.push(entry.name + suffix);
335
694
  } else {
336
695
  items.push(entry.name);
337
696
  }
@@ -348,7 +707,6 @@ export async function processCommand(
348
707
  if (command === 'tree') {
349
708
  const showFiles = parts.includes('-A') || parts.includes('--all');
350
709
 
351
- // Parse depth: --depth=N or -d N
352
710
  let maxDepth = 10;
353
711
  const depthFlag = parts.find(p => p.startsWith('--depth='));
354
712
  if (depthFlag) {
@@ -360,20 +718,18 @@ export async function processCommand(
360
718
  }
361
719
  }
362
720
 
363
- const treeLines = getTree(currentPath, '', true, maxDepth, 0, showFiles);
721
+ const treeLines = getTreeWithStatus(currentPath, '', true, maxDepth, 0, showFiles);
364
722
  if (treeLines.length === 0) {
365
723
  return wrapResult({ output: 'No items found.' });
366
724
  }
367
725
  return wrapResult({ output: treeLines.join('\n') });
368
726
  }
369
727
 
370
- // Handle navigation without 'cd' command (.., ..., ...., etc)
728
+ // Handle navigation without 'cd' command
371
729
  if (/^\.{2,}$/.test(command)) {
372
- // Count dots: .. = 1 level, ... = 2 levels, .... = 3 levels, etc
373
730
  const levels = command.length - 1;
374
731
  let targetPath = currentPath;
375
732
 
376
- // Already at root?
377
733
  if (targetPath === config.storagePath) {
378
734
  return { output: 'Already at root.' };
379
735
  }
@@ -381,14 +737,12 @@ export async function processCommand(
381
737
  for (let i = 0; i < levels; i++) {
382
738
  const parentPath = path.dirname(targetPath);
383
739
 
384
- // Stop at storage root
385
740
  if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
386
741
  break;
387
742
  }
388
743
 
389
744
  targetPath = parentPath;
390
745
 
391
- // If we reached root, stop
392
746
  if (targetPath === config.storagePath) {
393
747
  break;
394
748
  }
@@ -455,7 +809,6 @@ export async function processCommand(
455
809
  fs.renameSync(sourcePath, destPath);
456
810
  return { output: `Moved: ${source} -> ${dest}` };
457
811
  } catch (error: any) {
458
- // If rename fails (cross-device), copy then delete
459
812
  try {
460
813
  copyRecursive(sourcePath, destPath);
461
814
  fs.rmSync(sourcePath, { recursive: true, force: true });
@@ -469,7 +822,6 @@ export async function processCommand(
469
822
  if (command === 'open') {
470
823
  const { exec } = require('child_process');
471
824
 
472
- // open or open . - open current folder in system file manager
473
825
  if (parts.length < 2 || parts[1] === '.') {
474
826
  exec(`open "${currentPath}"`);
475
827
  return { output: `Opening: ${currentPath}` };
@@ -478,18 +830,15 @@ export async function processCommand(
478
830
  const name = parts.slice(1).join(' ');
479
831
  const targetPath = path.join(currentPath, name);
480
832
 
481
- // Check if target exists
482
833
  if (fs.existsSync(targetPath)) {
483
834
  const stat = fs.statSync(targetPath);
484
835
 
485
836
  if (stat.isDirectory()) {
486
- // It's a folder, open in file manager
487
837
  exec(`open "${targetPath}"`);
488
838
  return { output: `Opening: ${targetPath}` };
489
839
  }
490
840
 
491
841
  if (stat.isFile()) {
492
- // It's a file, show its content (supports | pbcopy)
493
842
  const content = fs.readFileSync(targetPath, 'utf-8');
494
843
  return wrapResult({ output: content });
495
844
  }
@@ -545,12 +894,10 @@ export async function processCommand(
545
894
 
546
895
  const target = parts.slice(1).join(' ');
547
896
 
548
- // Handle cd .., cd ..., cd ...., etc
549
897
  if (/^\.{2,}$/.test(target)) {
550
898
  const levels = target.length - 1;
551
899
  let targetPath = currentPath;
552
900
 
553
- // Already at root?
554
901
  if (targetPath === config.storagePath) {
555
902
  return { output: 'Already at root.' };
556
903
  }
@@ -558,14 +905,12 @@ export async function processCommand(
558
905
  for (let i = 0; i < levels; i++) {
559
906
  const parentPath = path.dirname(targetPath);
560
907
 
561
- // Stop at storage root
562
908
  if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
563
909
  break;
564
910
  }
565
911
 
566
912
  targetPath = parentPath;
567
913
 
568
- // If we reached root, stop
569
914
  if (targetPath === config.storagePath) {
570
915
  break;
571
916
  }
@@ -574,14 +919,12 @@ export async function processCommand(
574
919
  return { newPath: targetPath, output: '' };
575
920
  }
576
921
 
577
- // Handle paths like "cd bank/account" or "cd ../other"
578
922
  if (target.includes('/')) {
579
923
  let targetPath = currentPath;
580
924
  const pathParts = target.split('/');
581
925
 
582
926
  for (const part of pathParts) {
583
927
  if (/^\.{2,}$/.test(part)) {
584
- // Handle .., ..., ...., etc in path
585
928
  const levels = part.length - 1;
586
929
  for (let i = 0; i < levels; i++) {
587
930
  if (targetPath === config.storagePath) break;
@@ -656,121 +999,69 @@ Storage: ${config.storagePath}
656
999
 
657
1000
  if (command === 'help') {
658
1001
  return wrapResult({
659
- output: `Commands:
660
- init - Initialize rlc (first time setup)
661
- ls - List all schemas, todos, and notes
662
- tree - Show directory tree structure
663
- tree -A - Show tree with files
664
- tree --depth=N - Limit tree depth (e.g., --depth=2)
665
- map - Dungeon map visualization
666
- cd <node> - Navigate into a node
667
- cd .. - Go back to parent
668
- pwd - Show current path
669
- open - Open current folder in Finder
670
- open <folder> - Open specific folder in Finder
671
- mkdir <name> - Create new folder
672
- cp <src> <dest> - Copy file or folder
673
- mv <src> <dest> - Move/rename file or folder
674
- rm <name> - Delete file
675
- rm -rf <name> - Delete folder recursively
676
- config - Show configuration
677
- config:apiKey=<key> - Set API key
678
- v, version - Show version
679
- <description> - Create schema/todo (AI generates preview)
680
- save - Save pending schema to disk
681
- cancel - Discard pending schema
682
- clean - Show items to delete in current folder
683
- clean --yes - Delete all items in current folder
684
- exit/quit - Exit the program
1002
+ output: `
1003
+ === ROGUELIKE CLI ===
685
1004
 
686
- Clipboard:
687
- ls | pbcopy - Copy output to clipboard (macOS)
688
- tree | pbcopy - Works with any command
689
- config | copy - Alternative for Windows
1005
+ Navigation:
1006
+ ls List tasks and files
1007
+ tree Show task tree with status
1008
+ tree -A Include files
1009
+ tree --depth=N Limit tree depth
1010
+ cd <task> Navigate into task
1011
+ cd .., ... Go back 1 or 2 levels
1012
+ pwd Show current path
1013
+ open Open folder in Finder
690
1014
 
691
- Workflow:
692
- 1. Type description (e.g., "todo: deploy app")
693
- 2. AI generates schema preview
694
- 3. Refine with more instructions if needed
695
- 4. Type "save" to save or "cancel" to discard
1015
+ Task Management:
1016
+ mkdir <name> Create new task
1017
+ done Mark current task as completed (recursive)
1018
+ deadline <date> Set deadline (today, tomorrow, +3d, Jan 15)
1019
+ boss Toggle boss/milestone status (3x XP)
1020
+ block [reason] Mark task as blocked
1021
+ unblock Remove blocked status
1022
+ status Show current task details
696
1023
 
697
- Examples:
1024
+ File Operations:
1025
+ cp <src> <dest> Copy file or folder
1026
+ mv <src> <dest> Move/rename
1027
+ rm <name> Delete file
1028
+ rm -rf <name> Delete folder
698
1029
 
699
- > todo opening company in delaware
1030
+ Gamification:
1031
+ stats Show XP, level, streaks
1032
+ achievements Show achievement list
1033
+ map Dungeon map view
1034
+ map --ai AI-generated dungeon map
700
1035
 
701
- ┌─ TODO opening company in delaware ───────────────────────────┐
702
- │ │
703
- ├── register business name │
704
- ├── file incorporation papers │
705
- ├── get EIN number │
706
- └── Branch: legal │
707
- └── open business bank account │
708
- │ │
709
- └───────────────────────────────────────────────────────────────┘
1036
+ Schema Generation:
1037
+ <description> AI generates todo/schema preview
1038
+ save Save pending schema
1039
+ cancel Discard pending schema
710
1040
 
711
- > yandex cloud production infrastructure
1041
+ Utility:
1042
+ init Setup wizard
1043
+ config Show settings
1044
+ clean --yes Clear current folder
1045
+ v, version Show version
1046
+ help This help
1047
+ exit, quit Exit
712
1048
 
713
- ┌─────────────────────────────────────────────────────────────┐
714
- │ Yandex Cloud │
715
- │ │
716
- │ ┌──────────────────┐ ┌──────────────────┐ │
717
- │ │ back-fastapi │ │ admin-next │ │
718
- │ │ (VM) │ │ (VM) │ │
719
- │ └────────┬─────────┘ └──────────────────┘ │
720
- │ │ │
721
- │ ├──────────────────┬─────────────────┐ │
722
- │ │ │ │ │
723
- │ ┌────────▼────────┐ ┌─────▼──────┐ ┌──────▼────────┐ │
724
- │ │ PostgreSQL │ │ Redis │ │ Cloudflare │ │
725
- │ │ (Existing DB) │ │ Cluster │ │ R2 Storage │ │
726
- │ └─────────────────┘ └────────────┘ └───────────────┘ │
727
- └─────────────────────────────────────────────────────────────┘
728
-
729
- > architecture production redis web application
730
-
731
- ┌─ Architecture production redis web application ────────────┐
732
- │ │
733
- ├── load-balancer │
734
- ├── web-servers │
735
- │ ├── app-server-1 │
736
- │ ├── app-server-2 │
737
- │ └── app-server-3 │
738
- ├── redis │
739
- │ ├── cache-cluster │
740
- │ └── session-store │
741
- └── database │
742
- ├── postgres-primary │
743
- └── postgres-replica │
744
- │ │
745
- └───────────────────────────────────────────────────────────────┘
746
-
747
- > kubernetes cluster with clusters postgres and redis
1049
+ Clipboard:
1050
+ <cmd> | pbcopy Copy output (macOS)
1051
+ <cmd> | clip Copy output (Windows)
748
1052
 
749
- ┌─────────────────────────────────────────────────────────────┐
750
- │ Kubernetes cluster with clusters postgres │
751
- │ │
752
- │ ┌──────────────┐ ┌──────────────┐ │
753
- │ │ postgres │ │ redis │ │
754
- │ │ │ │ │ │
755
- │ │ primary-pod │ │ cache-pod-1 │ │
756
- │ │ replica-pod-1│ │ cache-pod-2 │ │
757
- │ │ replica-pod-2│ │ │ │
758
- │ └──────┬───────┘ └──────┬───────┘ │
759
- │ │ │ │
760
- │ └──────────┬───────────┘ │
761
- │ │ │
762
- │ ┌───────▼────────┐ │
763
- │ │ worker-zones │ │
764
- │ │ zone-1 │ │
765
- │ │ zone-2 │ │
766
- │ └────────────────┘ │
767
- └─────────────────────────────────────────────────────────────┘
1053
+ Deadlines:
1054
+ deadline today Due today
1055
+ deadline tomorrow Due tomorrow
1056
+ deadline +3d Due in 3 days
1057
+ deadline Jan 15 Due on date
768
1058
 
769
- www.rlc.rocks`
1059
+ www.rlc.rocks
1060
+ `.trim()
770
1061
  });
771
1062
  }
772
1063
 
773
- // Save command - save pending schema/todo
1064
+ // Save command
774
1065
  if (command === 'save') {
775
1066
  if (!sessionState.pending) {
776
1067
  return wrapResult({ output: 'Nothing to save. Create a schema first.' });
@@ -780,17 +1071,14 @@ www.rlc.rocks`
780
1071
  const safeName = pending.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
781
1072
 
782
1073
  if (pending.format === 'tree') {
783
- // Create folder structure from tree ASCII art
784
1074
  const rootPath = path.join(currentPath, safeName);
785
1075
  createFoldersFromTree(rootPath, pending.content);
786
1076
 
787
- // Clear session
788
1077
  sessionState.pending = null;
789
1078
  sessionState.history = [];
790
1079
 
791
1080
  return wrapResult({ output: `Created todo folder: ${safeName}/` });
792
1081
  } else {
793
- // Save as .rlc.schema file
794
1082
  const schemaPath = saveSchemaFile(
795
1083
  currentPath,
796
1084
  pending.title,
@@ -798,7 +1086,6 @@ www.rlc.rocks`
798
1086
  );
799
1087
  const filename = path.basename(schemaPath);
800
1088
 
801
- // Clear session
802
1089
  sessionState.pending = null;
803
1090
  sessionState.history = [];
804
1091
 
@@ -806,7 +1093,7 @@ www.rlc.rocks`
806
1093
  }
807
1094
  }
808
1095
 
809
- // Cancel command - discard pending schema
1096
+ // Cancel command
810
1097
  if (command === 'cancel') {
811
1098
  if (!sessionState.pending) {
812
1099
  return wrapResult({ output: 'Nothing to cancel.' });
@@ -818,7 +1105,7 @@ www.rlc.rocks`
818
1105
  return wrapResult({ output: 'Discarded pending schema.' });
819
1106
  }
820
1107
 
821
- // Clean command - clear current directory
1108
+ // Clean command
822
1109
  if (command === 'clean') {
823
1110
  const entries = fs.readdirSync(currentPath);
824
1111
  const toDelete = entries.filter(e => !e.startsWith('.'));
@@ -827,7 +1114,6 @@ www.rlc.rocks`
827
1114
  return wrapResult({ output: 'Directory is already empty.' });
828
1115
  }
829
1116
 
830
- // Check for --yes flag to skip confirmation
831
1117
  if (!parts.includes('--yes') && !parts.includes('-y')) {
832
1118
  return wrapResult({
833
1119
  output: `Will delete ${toDelete.length} items:\n${toDelete.join('\n')}\n\nRun "clean --yes" to confirm.`
@@ -842,10 +1128,9 @@ www.rlc.rocks`
842
1128
  return wrapResult({ output: `Deleted ${toDelete.length} items.` });
843
1129
  }
844
1130
 
845
- // AI generation - store in pending, don't save immediately
1131
+ // AI generation
846
1132
  const fullInput = cleanInput;
847
1133
 
848
- // Add user message to history
849
1134
  sessionState.history.push({ role: 'user', content: fullInput });
850
1135
 
851
1136
  const schema = await generateSchemaWithAI(fullInput, config, signal, sessionState.history);
@@ -855,7 +1140,6 @@ www.rlc.rocks`
855
1140
  }
856
1141
 
857
1142
  if (schema) {
858
- // Store in pending
859
1143
  sessionState.pending = {
860
1144
  title: schema.title,
861
1145
  content: schema.content,
@@ -863,7 +1147,6 @@ www.rlc.rocks`
863
1147
  tree: schema.tree
864
1148
  };
865
1149
 
866
- // Add assistant response to history
867
1150
  sessionState.history.push({ role: 'assistant', content: schema.content });
868
1151
 
869
1152
  const safeName = schema.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');