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.
@@ -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,24 +718,34 @@ 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 (.., ...)
371
- if (command === '..' || command === '...') {
372
- let levels = command === '...' ? 2 : 1;
728
+ // Handle navigation without 'cd' command
729
+ if (/^\.{2,}$/.test(command)) {
730
+ const levels = command.length - 1;
373
731
  let targetPath = currentPath;
374
732
 
733
+ if (targetPath === config.storagePath) {
734
+ return { output: 'Already at root.' };
735
+ }
736
+
375
737
  for (let i = 0; i < levels; i++) {
376
738
  const parentPath = path.dirname(targetPath);
377
- if (parentPath === config.storagePath || parentPath.length < config.storagePath.length) {
378
- return { output: 'Already at root.' };
739
+
740
+ if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
741
+ break;
379
742
  }
743
+
380
744
  targetPath = parentPath;
745
+
746
+ if (targetPath === config.storagePath) {
747
+ break;
748
+ }
381
749
  }
382
750
 
383
751
  return { newPath: targetPath, output: '' };
@@ -441,7 +809,6 @@ export async function processCommand(
441
809
  fs.renameSync(sourcePath, destPath);
442
810
  return { output: `Moved: ${source} -> ${dest}` };
443
811
  } catch (error: any) {
444
- // If rename fails (cross-device), copy then delete
445
812
  try {
446
813
  copyRecursive(sourcePath, destPath);
447
814
  fs.rmSync(sourcePath, { recursive: true, force: true });
@@ -455,7 +822,6 @@ export async function processCommand(
455
822
  if (command === 'open') {
456
823
  const { exec } = require('child_process');
457
824
 
458
- // open or open . - open current folder in system file manager
459
825
  if (parts.length < 2 || parts[1] === '.') {
460
826
  exec(`open "${currentPath}"`);
461
827
  return { output: `Opening: ${currentPath}` };
@@ -464,18 +830,15 @@ export async function processCommand(
464
830
  const name = parts.slice(1).join(' ');
465
831
  const targetPath = path.join(currentPath, name);
466
832
 
467
- // Check if target exists
468
833
  if (fs.existsSync(targetPath)) {
469
834
  const stat = fs.statSync(targetPath);
470
835
 
471
836
  if (stat.isDirectory()) {
472
- // It's a folder, open in file manager
473
837
  exec(`open "${targetPath}"`);
474
838
  return { output: `Opening: ${targetPath}` };
475
839
  }
476
840
 
477
841
  if (stat.isFile()) {
478
- // It's a file, show its content (supports | pbcopy)
479
842
  const content = fs.readFileSync(targetPath, 'utf-8');
480
843
  return wrapResult({ output: content });
481
844
  }
@@ -531,34 +894,47 @@ export async function processCommand(
531
894
 
532
895
  const target = parts.slice(1).join(' ');
533
896
 
534
- if (target === '..') {
535
- const parentPath = path.dirname(currentPath);
536
- if (parentPath === config.storagePath || parentPath.length < config.storagePath.length) {
897
+ if (/^\.{2,}$/.test(target)) {
898
+ const levels = target.length - 1;
899
+ let targetPath = currentPath;
900
+
901
+ if (targetPath === config.storagePath) {
537
902
  return { output: 'Already at root.' };
538
903
  }
539
- return { newPath: parentPath, output: '' };
540
- }
541
-
542
- if (target === '...') {
543
- let targetPath = path.dirname(currentPath);
544
- targetPath = path.dirname(targetPath);
545
- if (targetPath.length < config.storagePath.length) {
546
- return { output: 'Already at root.' };
904
+
905
+ for (let i = 0; i < levels; i++) {
906
+ const parentPath = path.dirname(targetPath);
907
+
908
+ if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
909
+ break;
910
+ }
911
+
912
+ targetPath = parentPath;
913
+
914
+ if (targetPath === config.storagePath) {
915
+ break;
916
+ }
547
917
  }
918
+
548
919
  return { newPath: targetPath, output: '' };
549
920
  }
550
921
 
551
- // Handle paths like "cd bank/account" or "cd ../other"
552
922
  if (target.includes('/')) {
553
923
  let targetPath = currentPath;
554
924
  const pathParts = target.split('/');
555
925
 
556
926
  for (const part of pathParts) {
557
- if (part === '..') {
558
- targetPath = path.dirname(targetPath);
927
+ if (/^\.{2,}$/.test(part)) {
928
+ const levels = part.length - 1;
929
+ for (let i = 0; i < levels; i++) {
930
+ if (targetPath === config.storagePath) break;
931
+ const parentPath = path.dirname(targetPath);
932
+ if (parentPath.length < config.storagePath.length) break;
933
+ targetPath = parentPath;
934
+ }
559
935
  } else if (part === '.') {
560
936
  continue;
561
- } else {
937
+ } else if (part) {
562
938
  const newPath = navigateToNode(targetPath, part);
563
939
  if (!newPath) {
564
940
  return { output: `Path "${target}" not found.` };
@@ -623,121 +999,69 @@ Storage: ${config.storagePath}
623
999
 
624
1000
  if (command === 'help') {
625
1001
  return wrapResult({
626
- output: `Commands:
627
- init - Initialize rlc (first time setup)
628
- ls - List all schemas, todos, and notes
629
- tree - Show directory tree structure
630
- tree -A - Show tree with files
631
- tree --depth=N - Limit tree depth (e.g., --depth=2)
632
- map - Dungeon map visualization
633
- cd <node> - Navigate into a node
634
- cd .. - Go back to parent
635
- pwd - Show current path
636
- open - Open current folder in Finder
637
- open <folder> - Open specific folder in Finder
638
- mkdir <name> - Create new folder
639
- cp <src> <dest> - Copy file or folder
640
- mv <src> <dest> - Move/rename file or folder
641
- rm <name> - Delete file
642
- rm -rf <name> - Delete folder recursively
643
- config - Show configuration
644
- config:apiKey=<key> - Set API key
645
- v, version - Show version
646
- <description> - Create schema/todo (AI generates preview)
647
- save - Save pending schema to disk
648
- cancel - Discard pending schema
649
- clean - Show items to delete in current folder
650
- clean --yes - Delete all items in current folder
651
- exit/quit - Exit the program
1002
+ output: `
1003
+ === ROGUELIKE CLI ===
652
1004
 
653
- Clipboard:
654
- ls | pbcopy - Copy output to clipboard (macOS)
655
- tree | pbcopy - Works with any command
656
- config | copy - Alternative for Windows
657
-
658
- Workflow:
659
- 1. Type description (e.g., "todo: deploy app")
660
- 2. AI generates schema preview
661
- 3. Refine with more instructions if needed
662
- 4. Type "save" to save or "cancel" to discard
663
-
664
- Examples:
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
665
1014
 
666
- > todo opening company in delaware
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
667
1023
 
668
- ┌─ TODO opening company in delaware ───────────────────────────┐
669
- │ │
670
- ├── register business name │
671
- ├── file incorporation papers │
672
- ├── get EIN number │
673
- └── Branch: legal │
674
- └── open business bank account │
675
- │ │
676
- └───────────────────────────────────────────────────────────────┘
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
677
1029
 
678
- > yandex cloud production infrastructure
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
679
1035
 
680
- ┌─────────────────────────────────────────────────────────────┐
681
- │ Yandex Cloud │
682
- │ │
683
- │ ┌──────────────────┐ ┌──────────────────┐ │
684
- │ │ back-fastapi │ │ admin-next │ │
685
- │ │ (VM) │ │ (VM) │ │
686
- │ └────────┬─────────┘ └──────────────────┘ │
687
- │ │ │
688
- │ ├──────────────────┬─────────────────┐ │
689
- │ │ │ │ │
690
- │ ┌────────▼────────┐ ┌─────▼──────┐ ┌──────▼────────┐ │
691
- │ │ PostgreSQL │ │ Redis │ │ Cloudflare │ │
692
- │ │ (Existing DB) │ │ Cluster │ │ R2 Storage │ │
693
- │ └─────────────────┘ └────────────┘ └───────────────┘ │
694
- └─────────────────────────────────────────────────────────────┘
1036
+ Schema Generation:
1037
+ <description> AI generates todo/schema preview
1038
+ save Save pending schema
1039
+ cancel Discard pending schema
695
1040
 
696
- > architecture production redis web application
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
697
1048
 
698
- ┌─ Architecture production redis web application ────────────┐
699
- │ │
700
- ├── load-balancer │
701
- ├── web-servers │
702
- │ ├── app-server-1 │
703
- │ ├── app-server-2 │
704
- │ └── app-server-3 │
705
- ├── redis │
706
- │ ├── cache-cluster │
707
- │ └── session-store │
708
- └── database │
709
- ├── postgres-primary │
710
- └── postgres-replica │
711
- │ │
712
- └───────────────────────────────────────────────────────────────┘
713
-
714
- > kubernetes cluster with clusters postgres and redis
1049
+ Clipboard:
1050
+ <cmd> | pbcopy Copy output (macOS)
1051
+ <cmd> | clip Copy output (Windows)
715
1052
 
716
- ┌─────────────────────────────────────────────────────────────┐
717
- │ Kubernetes cluster with clusters postgres │
718
- │ │
719
- │ ┌──────────────┐ ┌──────────────┐ │
720
- │ │ postgres │ │ redis │ │
721
- │ │ │ │ │ │
722
- │ │ primary-pod │ │ cache-pod-1 │ │
723
- │ │ replica-pod-1│ │ cache-pod-2 │ │
724
- │ │ replica-pod-2│ │ │ │
725
- │ └──────┬───────┘ └──────┬───────┘ │
726
- │ │ │ │
727
- │ └──────────┬───────────┘ │
728
- │ │ │
729
- │ ┌───────▼────────┐ │
730
- │ │ worker-zones │ │
731
- │ │ zone-1 │ │
732
- │ │ zone-2 │ │
733
- │ └────────────────┘ │
734
- └─────────────────────────────────────────────────────────────┘
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
735
1058
 
736
- www.rlc.rocks`
1059
+ www.rlc.rocks
1060
+ `.trim()
737
1061
  });
738
1062
  }
739
1063
 
740
- // Save command - save pending schema/todo
1064
+ // Save command
741
1065
  if (command === 'save') {
742
1066
  if (!sessionState.pending) {
743
1067
  return wrapResult({ output: 'Nothing to save. Create a schema first.' });
@@ -747,17 +1071,14 @@ www.rlc.rocks`
747
1071
  const safeName = pending.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
748
1072
 
749
1073
  if (pending.format === 'tree') {
750
- // Create folder structure from tree ASCII art
751
1074
  const rootPath = path.join(currentPath, safeName);
752
1075
  createFoldersFromTree(rootPath, pending.content);
753
1076
 
754
- // Clear session
755
1077
  sessionState.pending = null;
756
1078
  sessionState.history = [];
757
1079
 
758
1080
  return wrapResult({ output: `Created todo folder: ${safeName}/` });
759
1081
  } else {
760
- // Save as .rlc.schema file
761
1082
  const schemaPath = saveSchemaFile(
762
1083
  currentPath,
763
1084
  pending.title,
@@ -765,7 +1086,6 @@ www.rlc.rocks`
765
1086
  );
766
1087
  const filename = path.basename(schemaPath);
767
1088
 
768
- // Clear session
769
1089
  sessionState.pending = null;
770
1090
  sessionState.history = [];
771
1091
 
@@ -773,7 +1093,7 @@ www.rlc.rocks`
773
1093
  }
774
1094
  }
775
1095
 
776
- // Cancel command - discard pending schema
1096
+ // Cancel command
777
1097
  if (command === 'cancel') {
778
1098
  if (!sessionState.pending) {
779
1099
  return wrapResult({ output: 'Nothing to cancel.' });
@@ -785,7 +1105,7 @@ www.rlc.rocks`
785
1105
  return wrapResult({ output: 'Discarded pending schema.' });
786
1106
  }
787
1107
 
788
- // Clean command - clear current directory
1108
+ // Clean command
789
1109
  if (command === 'clean') {
790
1110
  const entries = fs.readdirSync(currentPath);
791
1111
  const toDelete = entries.filter(e => !e.startsWith('.'));
@@ -794,7 +1114,6 @@ www.rlc.rocks`
794
1114
  return wrapResult({ output: 'Directory is already empty.' });
795
1115
  }
796
1116
 
797
- // Check for --yes flag to skip confirmation
798
1117
  if (!parts.includes('--yes') && !parts.includes('-y')) {
799
1118
  return wrapResult({
800
1119
  output: `Will delete ${toDelete.length} items:\n${toDelete.join('\n')}\n\nRun "clean --yes" to confirm.`
@@ -809,10 +1128,9 @@ www.rlc.rocks`
809
1128
  return wrapResult({ output: `Deleted ${toDelete.length} items.` });
810
1129
  }
811
1130
 
812
- // AI generation - store in pending, don't save immediately
1131
+ // AI generation
813
1132
  const fullInput = cleanInput;
814
1133
 
815
- // Add user message to history
816
1134
  sessionState.history.push({ role: 'user', content: fullInput });
817
1135
 
818
1136
  const schema = await generateSchemaWithAI(fullInput, config, signal, sessionState.history);
@@ -822,7 +1140,6 @@ www.rlc.rocks`
822
1140
  }
823
1141
 
824
1142
  if (schema) {
825
- // Store in pending
826
1143
  sessionState.pending = {
827
1144
  title: schema.title,
828
1145
  content: schema.content,
@@ -830,7 +1147,6 @@ www.rlc.rocks`
830
1147
  tree: schema.tree
831
1148
  };
832
1149
 
833
- // Add assistant response to history
834
1150
  sessionState.history.push({ role: 'assistant', content: schema.content });
835
1151
 
836
1152
  const safeName = schema.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');