roguelike-cli 1.3.0 → 1.3.1

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.
package/README.md CHANGED
@@ -95,11 +95,13 @@ You are now level 2!
95
95
  | Command | Description |
96
96
  |---------|-------------|
97
97
  | `done` | Complete task (recursive, earns XP) |
98
+ | `undo` | Undo last done (restores XP) |
98
99
  | `deadline <date>` | Set deadline |
99
100
  | `boss` | Toggle boss status (3x XP) |
100
- | `block [reason]` | Mark as blocked |
101
+ | `block [node]` | Block by task or text reason |
101
102
  | `unblock` | Remove blocked status |
102
103
  | `status` | Show task details |
104
+ | `check` | Show overdue/upcoming deadlines |
103
105
 
104
106
  ### Gamification
105
107
 
@@ -428,6 +428,13 @@ async function processCommand(input, currentPath, config, signal, rl) {
428
428
  // Update profile with XP and achievements
429
429
  const depth = getDepth(currentPath, config.storagePath);
430
430
  const taskResult = (0, profile_1.completeTask)(result.xpGained, nodeConfig.isBoss || false, depth, nodeConfig.createdAt);
431
+ // Save to undo history
432
+ (0, profile_1.addToUndoHistory)({
433
+ path: currentPath,
434
+ xpLost: result.xpGained,
435
+ wasBoss: nodeConfig.isBoss || false,
436
+ timestamp: new Date().toISOString(),
437
+ });
431
438
  let output = `\n=== TASK COMPLETED ===\n`;
432
439
  output += `\nTasks completed: ${result.tasksCompleted}`;
433
440
  if (result.bossesDefeated > 0) {
@@ -445,6 +452,30 @@ async function processCommand(input, currentPath, config, signal, rl) {
445
452
  }
446
453
  }
447
454
  output += '\n';
455
+ output += '[Type "undo" to revert]';
456
+ return wrapResult({ output });
457
+ }
458
+ // Undo command
459
+ if (command === 'undo') {
460
+ const lastUndo = (0, profile_1.getLastUndo)();
461
+ if (!lastUndo) {
462
+ return wrapResult({ output: 'Nothing to undo.' });
463
+ }
464
+ // Revert the task status
465
+ const nodeConfig = (0, nodeConfig_1.readNodeConfig)(lastUndo.path);
466
+ if (nodeConfig) {
467
+ (0, nodeConfig_1.writeNodeConfig)(lastUndo.path, {
468
+ ...nodeConfig,
469
+ status: 'open',
470
+ completedAt: undefined,
471
+ });
472
+ }
473
+ // Revert profile stats
474
+ const result = (0, profile_1.performUndo)();
475
+ let output = `\n=== UNDO ===\n`;
476
+ output += `\nReverted: ${path.basename(lastUndo.path)}`;
477
+ output += `\n${result.message}`;
478
+ output += '\n';
448
479
  return wrapResult({ output });
449
480
  }
450
481
  // Deadline command
@@ -482,19 +513,48 @@ async function processCommand(input, currentPath, config, signal, rl) {
482
513
  });
483
514
  return wrapResult({ output: newIsBoss ? 'Marked as BOSS task (3x XP)' : 'Removed BOSS status' });
484
515
  }
485
- // Block command
516
+ // Block command - block <node> or block "reason"
486
517
  if (command === 'block') {
487
518
  const nodeConfig = (0, nodeConfig_1.readNodeConfig)(currentPath);
488
519
  if (!nodeConfig) {
489
520
  return wrapResult({ output: 'No task at current location.' });
490
521
  }
491
- const reason = parts.length > 1 ? parts.slice(1).join(' ') : undefined;
522
+ const arg = parts.length > 1 ? parts.slice(1).join(' ') : undefined;
523
+ let blockedBy = nodeConfig.blockedBy || [];
524
+ let blockMessage = 'Task marked as blocked';
525
+ if (arg) {
526
+ // Check if it's a path to another node
527
+ const potentialPath = path.join(currentPath, '..', arg);
528
+ const absolutePath = path.isAbsolute(arg) ? arg : potentialPath;
529
+ if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
530
+ // It's a node path
531
+ const blockerConfig = (0, nodeConfig_1.readNodeConfig)(absolutePath);
532
+ const blockerName = blockerConfig?.name || path.basename(absolutePath);
533
+ blockedBy = [...blockedBy, absolutePath];
534
+ blockMessage = `Blocked by: ${blockerName}`;
535
+ }
536
+ else {
537
+ // Check sibling folder
538
+ const siblingPath = path.join(path.dirname(currentPath), arg);
539
+ if (fs.existsSync(siblingPath) && fs.statSync(siblingPath).isDirectory()) {
540
+ const blockerConfig = (0, nodeConfig_1.readNodeConfig)(siblingPath);
541
+ const blockerName = blockerConfig?.name || arg;
542
+ blockedBy = [...blockedBy, siblingPath];
543
+ blockMessage = `Blocked by: ${blockerName}`;
544
+ }
545
+ else {
546
+ // It's a text reason
547
+ blockedBy = [...blockedBy, arg];
548
+ blockMessage = `Blocked: ${arg}`;
549
+ }
550
+ }
551
+ }
492
552
  (0, nodeConfig_1.writeNodeConfig)(currentPath, {
493
553
  ...nodeConfig,
494
554
  status: 'blocked',
495
- blockedBy: reason ? [reason] : nodeConfig.blockedBy,
555
+ blockedBy,
496
556
  });
497
- return wrapResult({ output: reason ? `Blocked: ${reason}` : 'Task marked as blocked' });
557
+ return wrapResult({ output: blockMessage });
498
558
  }
499
559
  // Unblock command
500
560
  if (command === 'unblock') {
@@ -509,6 +569,67 @@ async function processCommand(input, currentPath, config, signal, rl) {
509
569
  });
510
570
  return wrapResult({ output: 'Task unblocked' });
511
571
  }
572
+ // Check command - show overdue/upcoming deadlines
573
+ if (command === 'check') {
574
+ const checkDeadlines = (dirPath, results) => {
575
+ if (!fs.existsSync(dirPath))
576
+ return;
577
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
578
+ for (const entry of entries) {
579
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
580
+ continue;
581
+ const nodePath = path.join(dirPath, entry.name);
582
+ const cfg = (0, nodeConfig_1.readNodeConfig)(nodePath);
583
+ if (cfg && cfg.deadline && cfg.status !== 'done') {
584
+ const deadlineDate = new Date(cfg.deadline);
585
+ const today = new Date();
586
+ today.setHours(0, 0, 0, 0);
587
+ deadlineDate.setHours(0, 0, 0, 0);
588
+ const diffDays = Math.ceil((deadlineDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
589
+ if (diffDays <= 3) { // Show tasks due within 3 days or overdue
590
+ results.push({
591
+ path: nodePath,
592
+ name: cfg.name,
593
+ deadline: formatDeadline(cfg.deadline),
594
+ status: diffDays < 0 ? 'OVERDUE' : diffDays === 0 ? 'TODAY' : 'SOON',
595
+ });
596
+ }
597
+ }
598
+ checkDeadlines(nodePath, results);
599
+ }
600
+ };
601
+ const results = [];
602
+ checkDeadlines(config.storagePath, results);
603
+ if (results.length === 0) {
604
+ return wrapResult({ output: 'No upcoming deadlines within 3 days.' });
605
+ }
606
+ const lines = ['', '=== DEADLINE CHECK ===', ''];
607
+ const overdue = results.filter(r => r.status === 'OVERDUE');
608
+ const today = results.filter(r => r.status === 'TODAY');
609
+ const soon = results.filter(r => r.status === 'SOON');
610
+ if (overdue.length > 0) {
611
+ lines.push('OVERDUE:');
612
+ for (const r of overdue) {
613
+ lines.push(` ! ${r.name} (${r.deadline})`);
614
+ }
615
+ lines.push('');
616
+ }
617
+ if (today.length > 0) {
618
+ lines.push('DUE TODAY:');
619
+ for (const r of today) {
620
+ lines.push(` * ${r.name}`);
621
+ }
622
+ lines.push('');
623
+ }
624
+ if (soon.length > 0) {
625
+ lines.push('UPCOMING:');
626
+ for (const r of soon) {
627
+ lines.push(` - ${r.name} (${r.deadline})`);
628
+ }
629
+ lines.push('');
630
+ }
631
+ return wrapResult({ output: lines.join('\n') });
632
+ }
512
633
  // Status command - show current task status
513
634
  if (command === 'status') {
514
635
  const nodeConfig = (0, nodeConfig_1.readNodeConfig)(currentPath);
@@ -531,7 +652,15 @@ async function processCommand(input, currentPath, config, signal, rl) {
531
652
  lines.push(`Completed: ${new Date(nodeConfig.completedAt).toLocaleDateString()}`);
532
653
  }
533
654
  if (nodeConfig.blockedBy && nodeConfig.blockedBy.length > 0) {
534
- lines.push(`Blocked by: ${nodeConfig.blockedBy.join(', ')}`);
655
+ // Format blockedBy - show names if paths exist
656
+ const blockerNames = nodeConfig.blockedBy.map(b => {
657
+ if (fs.existsSync(b)) {
658
+ const cfg = (0, nodeConfig_1.readNodeConfig)(b);
659
+ return cfg?.name || path.basename(b);
660
+ }
661
+ return b;
662
+ });
663
+ lines.push(`Blocked by: ${blockerNames.join(', ')}`);
535
664
  }
536
665
  lines.push('');
537
666
  return wrapResult({ output: lines.join('\n') });
@@ -850,11 +979,13 @@ Navigation:
850
979
  Task Management:
851
980
  mkdir <name> Create new task
852
981
  done Mark current task as completed (recursive)
982
+ undo Undo last done (restores XP)
853
983
  deadline <date> Set deadline (today, tomorrow, +3d, Jan 15)
854
984
  boss Toggle boss/milestone status (3x XP)
855
- block [reason] Mark task as blocked
985
+ block [node] Block by task (or text reason)
856
986
  unblock Remove blocked status
857
987
  status Show current task details
988
+ check Show overdue/upcoming deadlines
858
989
 
859
990
  File Operations:
860
991
  cp <src> <dest> Copy file or folder
@@ -885,11 +1016,11 @@ Clipboard:
885
1016
  <cmd> | pbcopy Copy output (macOS)
886
1017
  <cmd> | clip Copy output (Windows)
887
1018
 
888
- Deadlines:
889
- deadline today Due today
890
- deadline tomorrow Due tomorrow
1019
+ Examples:
1020
+ block backend-api Block current task by sibling task
1021
+ block "waiting for design" Block with text reason
891
1022
  deadline +3d Due in 3 days
892
- deadline Jan 15 Due on date
1023
+ check See all upcoming deadlines
893
1024
 
894
1025
  www.rlc.rocks
895
1026
  `.trim()
@@ -20,8 +20,8 @@ const ASCII_ART = [
20
20
  '╚═════════════════════════╝',
21
21
  '',
22
22
  ' Navigation: ls, cd, tree, pwd, open',
23
- ' Tasks: done, deadline, boss, block, status',
24
- ' Gamification: stats, achievements, map',
23
+ ' Tasks: done, undo, deadline, boss, block',
24
+ ' Gamification: stats, achievements, map, check',
25
25
  '',
26
26
  ' TAB to autocomplete, | pbcopy to copy',
27
27
  ' Workflow: <description> -> refine -> save',
@@ -43,6 +43,9 @@ exports.addXP = addXP;
43
43
  exports.completeTask = completeTask;
44
44
  exports.formatStats = formatStats;
45
45
  exports.formatAchievements = formatAchievements;
46
+ exports.addToUndoHistory = addToUndoHistory;
47
+ exports.getLastUndo = getLastUndo;
48
+ exports.performUndo = performUndo;
46
49
  const fs = __importStar(require("fs"));
47
50
  const path = __importStar(require("path"));
48
51
  const os = __importStar(require("os"));
@@ -127,6 +130,7 @@ function createDefaultProfile() {
127
130
  currentStreak: 0,
128
131
  longestStreak: 0,
129
132
  achievements: [],
133
+ undoHistory: [],
130
134
  stats: {
131
135
  completedByDay: {},
132
136
  createdAt: new Date().toISOString(),
@@ -274,3 +278,46 @@ function formatAchievements() {
274
278
  lines.push('');
275
279
  return lines.join('\n');
276
280
  }
281
+ function addToUndoHistory(entry) {
282
+ const profile = readProfile();
283
+ // Keep only last 10 undo entries
284
+ profile.undoHistory = profile.undoHistory || [];
285
+ profile.undoHistory.unshift(entry);
286
+ if (profile.undoHistory.length > 10) {
287
+ profile.undoHistory = profile.undoHistory.slice(0, 10);
288
+ }
289
+ saveProfile(profile);
290
+ }
291
+ function getLastUndo() {
292
+ const profile = readProfile();
293
+ if (!profile.undoHistory || profile.undoHistory.length === 0) {
294
+ return null;
295
+ }
296
+ return profile.undoHistory[0];
297
+ }
298
+ function performUndo() {
299
+ const profile = readProfile();
300
+ if (!profile.undoHistory || profile.undoHistory.length === 0) {
301
+ return { success: false, entry: null, message: 'Nothing to undo.' };
302
+ }
303
+ const entry = profile.undoHistory.shift();
304
+ // Subtract XP
305
+ profile.totalXP = Math.max(0, profile.totalXP - entry.xpLost);
306
+ profile.tasksCompleted = Math.max(0, profile.tasksCompleted - 1);
307
+ if (entry.wasBoss) {
308
+ profile.bossesDefeated = Math.max(0, profile.bossesDefeated - 1);
309
+ }
310
+ // Update level
311
+ profile.level = levelFromXP(profile.totalXP);
312
+ // Update daily stats
313
+ const today = new Date().toISOString().split('T')[0];
314
+ if (profile.stats.completedByDay[today]) {
315
+ profile.stats.completedByDay[today] = Math.max(0, profile.stats.completedByDay[today] - 1);
316
+ }
317
+ saveProfile(profile);
318
+ return {
319
+ success: true,
320
+ entry,
321
+ message: `Undo: -${entry.xpLost} XP`
322
+ };
323
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roguelike-cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "AI-powered interactive terminal for creating schemas and todo lists",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -6,7 +6,7 @@ import { Config } from '../config/config';
6
6
  import { listSchemas, navigateToNode, getTree } from '../storage/storage';
7
7
  import { saveSchemaFile, writeNodeConfig, readNodeConfig, calculateXP, saveMapFile, NodeConfig } from '../storage/nodeConfig';
8
8
  import { generateSchemaWithAI, generateDungeonMapWithAI } from '../ai/claude';
9
- import { completeTask, formatStats, formatAchievements, readProfile } from '../storage/profile';
9
+ import { completeTask, formatStats, formatAchievements, readProfile, addToUndoHistory, performUndo, getLastUndo } from '../storage/profile';
10
10
 
11
11
  // Parse tree ASCII art and create folder structure
12
12
  function createFoldersFromTree(rootPath: string, treeContent: string): void {
@@ -505,6 +505,14 @@ export async function processCommand(
505
505
  nodeConfig.createdAt
506
506
  );
507
507
 
508
+ // Save to undo history
509
+ addToUndoHistory({
510
+ path: currentPath,
511
+ xpLost: result.xpGained,
512
+ wasBoss: nodeConfig.isBoss || false,
513
+ timestamp: new Date().toISOString(),
514
+ });
515
+
508
516
  let output = `\n=== TASK COMPLETED ===\n`;
509
517
  output += `\nTasks completed: ${result.tasksCompleted}`;
510
518
  if (result.bossesDefeated > 0) {
@@ -524,6 +532,35 @@ export async function processCommand(
524
532
  }
525
533
  }
526
534
 
535
+ output += '\n';
536
+ output += '[Type "undo" to revert]';
537
+
538
+ return wrapResult({ output });
539
+ }
540
+
541
+ // Undo command
542
+ if (command === 'undo') {
543
+ const lastUndo = getLastUndo();
544
+ if (!lastUndo) {
545
+ return wrapResult({ output: 'Nothing to undo.' });
546
+ }
547
+
548
+ // Revert the task status
549
+ const nodeConfig = readNodeConfig(lastUndo.path);
550
+ if (nodeConfig) {
551
+ writeNodeConfig(lastUndo.path, {
552
+ ...nodeConfig,
553
+ status: 'open',
554
+ completedAt: undefined,
555
+ });
556
+ }
557
+
558
+ // Revert profile stats
559
+ const result = performUndo();
560
+
561
+ let output = `\n=== UNDO ===\n`;
562
+ output += `\nReverted: ${path.basename(lastUndo.path)}`;
563
+ output += `\n${result.message}`;
527
564
  output += '\n';
528
565
 
529
566
  return wrapResult({ output });
@@ -574,22 +611,51 @@ export async function processCommand(
574
611
  return wrapResult({ output: newIsBoss ? 'Marked as BOSS task (3x XP)' : 'Removed BOSS status' });
575
612
  }
576
613
 
577
- // Block command
614
+ // Block command - block <node> or block "reason"
578
615
  if (command === 'block') {
579
616
  const nodeConfig = readNodeConfig(currentPath);
580
617
  if (!nodeConfig) {
581
618
  return wrapResult({ output: 'No task at current location.' });
582
619
  }
583
620
 
584
- const reason = parts.length > 1 ? parts.slice(1).join(' ') : undefined;
621
+ const arg = parts.length > 1 ? parts.slice(1).join(' ') : undefined;
622
+ let blockedBy: string[] = nodeConfig.blockedBy || [];
623
+ let blockMessage = 'Task marked as blocked';
624
+
625
+ if (arg) {
626
+ // Check if it's a path to another node
627
+ const potentialPath = path.join(currentPath, '..', arg);
628
+ const absolutePath = path.isAbsolute(arg) ? arg : potentialPath;
629
+
630
+ if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
631
+ // It's a node path
632
+ const blockerConfig = readNodeConfig(absolutePath);
633
+ const blockerName = blockerConfig?.name || path.basename(absolutePath);
634
+ blockedBy = [...blockedBy, absolutePath];
635
+ blockMessage = `Blocked by: ${blockerName}`;
636
+ } else {
637
+ // Check sibling folder
638
+ const siblingPath = path.join(path.dirname(currentPath), arg);
639
+ if (fs.existsSync(siblingPath) && fs.statSync(siblingPath).isDirectory()) {
640
+ const blockerConfig = readNodeConfig(siblingPath);
641
+ const blockerName = blockerConfig?.name || arg;
642
+ blockedBy = [...blockedBy, siblingPath];
643
+ blockMessage = `Blocked by: ${blockerName}`;
644
+ } else {
645
+ // It's a text reason
646
+ blockedBy = [...blockedBy, arg];
647
+ blockMessage = `Blocked: ${arg}`;
648
+ }
649
+ }
650
+ }
585
651
 
586
652
  writeNodeConfig(currentPath, {
587
653
  ...nodeConfig,
588
654
  status: 'blocked',
589
- blockedBy: reason ? [reason] : nodeConfig.blockedBy,
655
+ blockedBy,
590
656
  });
591
657
 
592
- return wrapResult({ output: reason ? `Blocked: ${reason}` : 'Task marked as blocked' });
658
+ return wrapResult({ output: blockMessage });
593
659
  }
594
660
 
595
661
  // Unblock command
@@ -608,6 +674,80 @@ export async function processCommand(
608
674
  return wrapResult({ output: 'Task unblocked' });
609
675
  }
610
676
 
677
+ // Check command - show overdue/upcoming deadlines
678
+ if (command === 'check') {
679
+ const checkDeadlines = (dirPath: string, results: { path: string; name: string; deadline: string; status: string }[]) => {
680
+ if (!fs.existsSync(dirPath)) return;
681
+
682
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
683
+ for (const entry of entries) {
684
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
685
+
686
+ const nodePath = path.join(dirPath, entry.name);
687
+ const cfg = readNodeConfig(nodePath);
688
+
689
+ if (cfg && cfg.deadline && cfg.status !== 'done') {
690
+ const deadlineDate = new Date(cfg.deadline);
691
+ const today = new Date();
692
+ today.setHours(0, 0, 0, 0);
693
+ deadlineDate.setHours(0, 0, 0, 0);
694
+
695
+ const diffDays = Math.ceil((deadlineDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
696
+
697
+ if (diffDays <= 3) { // Show tasks due within 3 days or overdue
698
+ results.push({
699
+ path: nodePath,
700
+ name: cfg.name,
701
+ deadline: formatDeadline(cfg.deadline),
702
+ status: diffDays < 0 ? 'OVERDUE' : diffDays === 0 ? 'TODAY' : 'SOON',
703
+ });
704
+ }
705
+ }
706
+
707
+ checkDeadlines(nodePath, results);
708
+ }
709
+ };
710
+
711
+ const results: { path: string; name: string; deadline: string; status: string }[] = [];
712
+ checkDeadlines(config.storagePath, results);
713
+
714
+ if (results.length === 0) {
715
+ return wrapResult({ output: 'No upcoming deadlines within 3 days.' });
716
+ }
717
+
718
+ const lines: string[] = ['', '=== DEADLINE CHECK ===', ''];
719
+
720
+ const overdue = results.filter(r => r.status === 'OVERDUE');
721
+ const today = results.filter(r => r.status === 'TODAY');
722
+ const soon = results.filter(r => r.status === 'SOON');
723
+
724
+ if (overdue.length > 0) {
725
+ lines.push('OVERDUE:');
726
+ for (const r of overdue) {
727
+ lines.push(` ! ${r.name} (${r.deadline})`);
728
+ }
729
+ lines.push('');
730
+ }
731
+
732
+ if (today.length > 0) {
733
+ lines.push('DUE TODAY:');
734
+ for (const r of today) {
735
+ lines.push(` * ${r.name}`);
736
+ }
737
+ lines.push('');
738
+ }
739
+
740
+ if (soon.length > 0) {
741
+ lines.push('UPCOMING:');
742
+ for (const r of soon) {
743
+ lines.push(` - ${r.name} (${r.deadline})`);
744
+ }
745
+ lines.push('');
746
+ }
747
+
748
+ return wrapResult({ output: lines.join('\n') });
749
+ }
750
+
611
751
  // Status command - show current task status
612
752
  if (command === 'status') {
613
753
  const nodeConfig = readNodeConfig(currentPath);
@@ -635,7 +775,15 @@ export async function processCommand(
635
775
  }
636
776
 
637
777
  if (nodeConfig.blockedBy && nodeConfig.blockedBy.length > 0) {
638
- lines.push(`Blocked by: ${nodeConfig.blockedBy.join(', ')}`);
778
+ // Format blockedBy - show names if paths exist
779
+ const blockerNames = nodeConfig.blockedBy.map(b => {
780
+ if (fs.existsSync(b)) {
781
+ const cfg = readNodeConfig(b);
782
+ return cfg?.name || path.basename(b);
783
+ }
784
+ return b;
785
+ });
786
+ lines.push(`Blocked by: ${blockerNames.join(', ')}`);
639
787
  }
640
788
 
641
789
  lines.push('');
@@ -1015,11 +1163,13 @@ Navigation:
1015
1163
  Task Management:
1016
1164
  mkdir <name> Create new task
1017
1165
  done Mark current task as completed (recursive)
1166
+ undo Undo last done (restores XP)
1018
1167
  deadline <date> Set deadline (today, tomorrow, +3d, Jan 15)
1019
1168
  boss Toggle boss/milestone status (3x XP)
1020
- block [reason] Mark task as blocked
1169
+ block [node] Block by task (or text reason)
1021
1170
  unblock Remove blocked status
1022
1171
  status Show current task details
1172
+ check Show overdue/upcoming deadlines
1023
1173
 
1024
1174
  File Operations:
1025
1175
  cp <src> <dest> Copy file or folder
@@ -1050,11 +1200,11 @@ Clipboard:
1050
1200
  <cmd> | pbcopy Copy output (macOS)
1051
1201
  <cmd> | clip Copy output (Windows)
1052
1202
 
1053
- Deadlines:
1054
- deadline today Due today
1055
- deadline tomorrow Due tomorrow
1203
+ Examples:
1204
+ block backend-api Block current task by sibling task
1205
+ block "waiting for design" Block with text reason
1056
1206
  deadline +3d Due in 3 days
1057
- deadline Jan 15 Due on date
1207
+ check See all upcoming deadlines
1058
1208
 
1059
1209
  www.rlc.rocks
1060
1210
  `.trim()
@@ -18,8 +18,8 @@ const ASCII_ART = [
18
18
  '╚═════════════════════════╝',
19
19
  '',
20
20
  ' Navigation: ls, cd, tree, pwd, open',
21
- ' Tasks: done, deadline, boss, block, status',
22
- ' Gamification: stats, achievements, map',
21
+ ' Tasks: done, undo, deadline, boss, block',
22
+ ' Gamification: stats, achievements, map, check',
23
23
  '',
24
24
  ' TAB to autocomplete, | pbcopy to copy',
25
25
  ' Workflow: <description> -> refine -> save',
@@ -9,6 +9,13 @@ export interface Achievement {
9
9
  unlockedAt?: string;
10
10
  }
11
11
 
12
+ export interface UndoEntry {
13
+ path: string;
14
+ xpLost: number;
15
+ wasBoss: boolean;
16
+ timestamp: string;
17
+ }
18
+
12
19
  export interface Profile {
13
20
  totalXP: number;
14
21
  level: number;
@@ -18,6 +25,7 @@ export interface Profile {
18
25
  longestStreak: number;
19
26
  lastCompletionDate?: string;
20
27
  achievements: string[];
28
+ undoHistory: UndoEntry[];
21
29
  stats: {
22
30
  completedByDay: Record<string, number>;
23
31
  createdAt: string;
@@ -114,6 +122,7 @@ function createDefaultProfile(): Profile {
114
122
  currentStreak: 0,
115
123
  longestStreak: 0,
116
124
  achievements: [],
125
+ undoHistory: [],
117
126
  stats: {
118
127
  completedByDay: {},
119
128
  createdAt: new Date().toISOString(),
@@ -279,3 +288,59 @@ export function formatAchievements(): string {
279
288
  return lines.join('\n');
280
289
  }
281
290
 
291
+ export function addToUndoHistory(entry: UndoEntry): void {
292
+ const profile = readProfile();
293
+
294
+ // Keep only last 10 undo entries
295
+ profile.undoHistory = profile.undoHistory || [];
296
+ profile.undoHistory.unshift(entry);
297
+ if (profile.undoHistory.length > 10) {
298
+ profile.undoHistory = profile.undoHistory.slice(0, 10);
299
+ }
300
+
301
+ saveProfile(profile);
302
+ }
303
+
304
+ export function getLastUndo(): UndoEntry | null {
305
+ const profile = readProfile();
306
+ if (!profile.undoHistory || profile.undoHistory.length === 0) {
307
+ return null;
308
+ }
309
+ return profile.undoHistory[0];
310
+ }
311
+
312
+ export function performUndo(): { success: boolean; entry: UndoEntry | null; message: string } {
313
+ const profile = readProfile();
314
+
315
+ if (!profile.undoHistory || profile.undoHistory.length === 0) {
316
+ return { success: false, entry: null, message: 'Nothing to undo.' };
317
+ }
318
+
319
+ const entry = profile.undoHistory.shift()!;
320
+
321
+ // Subtract XP
322
+ profile.totalXP = Math.max(0, profile.totalXP - entry.xpLost);
323
+ profile.tasksCompleted = Math.max(0, profile.tasksCompleted - 1);
324
+
325
+ if (entry.wasBoss) {
326
+ profile.bossesDefeated = Math.max(0, profile.bossesDefeated - 1);
327
+ }
328
+
329
+ // Update level
330
+ profile.level = levelFromXP(profile.totalXP);
331
+
332
+ // Update daily stats
333
+ const today = new Date().toISOString().split('T')[0];
334
+ if (profile.stats.completedByDay[today]) {
335
+ profile.stats.completedByDay[today] = Math.max(0, profile.stats.completedByDay[today] - 1);
336
+ }
337
+
338
+ saveProfile(profile);
339
+
340
+ return {
341
+ success: true,
342
+ entry,
343
+ message: `Undo: -${entry.xpLost} XP`
344
+ };
345
+ }
346
+