roguelike-cli 1.3.0 → 1.3.2
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 +126 -116
- package/dist/ai/claude.js +14 -4
- package/dist/commands/init.js +39 -6
- package/dist/config/config.js +32 -0
- package/dist/interactive/commands.js +214 -60
- package/dist/interactive/startup.js +7 -7
- package/dist/storage/profile.js +47 -0
- package/package.json +1 -1
- package/src/ai/claude.ts +18 -4
- package/src/commands/init.ts +43 -7
- package/src/config/config.ts +34 -0
- package/src/interactive/commands.ts +241 -66
- package/src/interactive/startup.ts +7 -7
- package/src/storage/profile.ts +65 -0
|
@@ -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
|
|
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
|
|
555
|
+
blockedBy,
|
|
496
556
|
});
|
|
497
|
-
return wrapResult({ output:
|
|
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
|
-
|
|
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') });
|
|
@@ -802,36 +931,67 @@ async function processCommand(input, currentPath, config, signal, rl) {
|
|
|
802
931
|
return wrapResult({ output: currentPath });
|
|
803
932
|
}
|
|
804
933
|
if (command === 'config') {
|
|
934
|
+
const { updateConfig, RULES_PRESETS } = await Promise.resolve().then(() => __importStar(require('../config/config')));
|
|
935
|
+
// Check for flags
|
|
936
|
+
const keyFlag = parts.find(p => p.startsWith('-k=') || p.startsWith('--key='));
|
|
937
|
+
const modelFlag = parts.find(p => p.startsWith('-m=') || p.startsWith('--model='));
|
|
938
|
+
const rulesFlag = parts.find(p => p.startsWith('-r=') || p.startsWith('--rules='));
|
|
939
|
+
const themeFlag = parts.find(p => p.startsWith('-t=') || p.startsWith('--theme='));
|
|
940
|
+
if (keyFlag) {
|
|
941
|
+
const value = keyFlag.split('=').slice(1).join('=');
|
|
942
|
+
updateConfig({ apiKey: value });
|
|
943
|
+
return wrapResult({ output: 'API key updated.' });
|
|
944
|
+
}
|
|
945
|
+
if (modelFlag) {
|
|
946
|
+
const value = modelFlag.split('=').slice(1).join('=');
|
|
947
|
+
updateConfig({ model: value });
|
|
948
|
+
return wrapResult({ output: `Model updated: ${value}` });
|
|
949
|
+
}
|
|
950
|
+
if (rulesFlag) {
|
|
951
|
+
const value = rulesFlag.split('=').slice(1).join('=');
|
|
952
|
+
updateConfig({ rules: value, rulesPreset: 'custom' });
|
|
953
|
+
return wrapResult({ output: 'Custom rules updated.' });
|
|
954
|
+
}
|
|
955
|
+
if (themeFlag) {
|
|
956
|
+
const value = themeFlag.split('=').slice(1).join('=').toLowerCase();
|
|
957
|
+
if (RULES_PRESETS[value]) {
|
|
958
|
+
updateConfig({
|
|
959
|
+
rules: RULES_PRESETS[value].rules,
|
|
960
|
+
rulesPreset: value
|
|
961
|
+
});
|
|
962
|
+
return wrapResult({ output: `Theme updated: ${RULES_PRESETS[value].name}` });
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
const themes = Object.keys(RULES_PRESETS).join(', ');
|
|
966
|
+
return wrapResult({ output: `Unknown theme. Available: ${themes}` });
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
// Show config
|
|
805
970
|
const maskedKey = config.apiKey
|
|
806
971
|
? config.apiKey.slice(0, 8) + '...' + config.apiKey.slice(-4)
|
|
807
972
|
: '(not set)';
|
|
973
|
+
const themeName = config.rulesPreset
|
|
974
|
+
? (RULES_PRESETS[config.rulesPreset]?.name || 'Custom')
|
|
975
|
+
: 'Default';
|
|
976
|
+
const rulesPreview = config.rules
|
|
977
|
+
? (config.rules.length > 50 ? config.rules.substring(0, 50) + '...' : config.rules)
|
|
978
|
+
: '(none)';
|
|
808
979
|
const output = `
|
|
809
980
|
Provider: ${config.aiProvider}
|
|
810
981
|
Model: ${config.model || '(default)'}
|
|
811
982
|
API Key: ${maskedKey}
|
|
812
983
|
Storage: ${config.storagePath}
|
|
984
|
+
Theme: ${themeName}
|
|
985
|
+
Rules: ${rulesPreview}
|
|
986
|
+
|
|
987
|
+
Set with flags:
|
|
988
|
+
config -k=<key> Set API key
|
|
989
|
+
config -m=<model> Set model
|
|
990
|
+
config -t=<theme> Set theme (fantasy, space, starwars, western, cyberpunk, pirate)
|
|
991
|
+
config -r="<rules>" Set custom rules
|
|
813
992
|
`.trim();
|
|
814
993
|
return wrapResult({ output });
|
|
815
994
|
}
|
|
816
|
-
if (command.startsWith('config:')) {
|
|
817
|
-
const configParts = input.split(':').slice(1).join(':').trim().split('=');
|
|
818
|
-
if (configParts.length !== 2) {
|
|
819
|
-
return { output: 'Usage: config:key=value' };
|
|
820
|
-
}
|
|
821
|
-
const key = configParts[0].trim();
|
|
822
|
-
const value = configParts[1].trim();
|
|
823
|
-
if (key === 'apiKey') {
|
|
824
|
-
const { updateConfig } = await Promise.resolve().then(() => __importStar(require('../config/config')));
|
|
825
|
-
updateConfig({ apiKey: value });
|
|
826
|
-
return { output: 'API key updated.' };
|
|
827
|
-
}
|
|
828
|
-
if (key === 'storagePath') {
|
|
829
|
-
const { updateConfig } = await Promise.resolve().then(() => __importStar(require('../config/config')));
|
|
830
|
-
updateConfig({ storagePath: value, currentPath: value });
|
|
831
|
-
return { output: `Storage path updated to: ${value}` };
|
|
832
|
-
}
|
|
833
|
-
return { output: `Unknown config key: ${key}` };
|
|
834
|
-
}
|
|
835
995
|
if (command === 'help') {
|
|
836
996
|
return wrapResult({
|
|
837
997
|
output: `
|
|
@@ -839,9 +999,7 @@ Storage: ${config.storagePath}
|
|
|
839
999
|
|
|
840
1000
|
Navigation:
|
|
841
1001
|
ls List tasks and files
|
|
842
|
-
tree
|
|
843
|
-
tree -A Include files
|
|
844
|
-
tree --depth=N Limit tree depth
|
|
1002
|
+
tree [-A] [--depth=N] Show task tree
|
|
845
1003
|
cd <task> Navigate into task
|
|
846
1004
|
cd .., ... Go back 1 or 2 levels
|
|
847
1005
|
pwd Show current path
|
|
@@ -849,47 +1007,43 @@ Navigation:
|
|
|
849
1007
|
|
|
850
1008
|
Task Management:
|
|
851
1009
|
mkdir <name> Create new task
|
|
852
|
-
done
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
cp <src> <dest> Copy file or folder
|
|
861
|
-
mv <src> <dest> Move/rename
|
|
862
|
-
rm <name> Delete file
|
|
863
|
-
rm -rf <name> Delete folder
|
|
1010
|
+
done Complete task (earns XP)
|
|
1011
|
+
undo Undo last done
|
|
1012
|
+
deadline <date> Set deadline (today, +3d, Jan 15)
|
|
1013
|
+
boss Toggle boss status (3x XP)
|
|
1014
|
+
block [node] Block by task or text
|
|
1015
|
+
unblock Remove block
|
|
1016
|
+
status Task details
|
|
1017
|
+
check Upcoming deadlines
|
|
864
1018
|
|
|
865
1019
|
Gamification:
|
|
866
|
-
stats
|
|
867
|
-
achievements
|
|
868
|
-
map
|
|
869
|
-
map --ai AI-generated dungeon map
|
|
870
|
-
|
|
871
|
-
Schema Generation:
|
|
872
|
-
<description> AI generates todo/schema preview
|
|
873
|
-
save Save pending schema
|
|
874
|
-
cancel Discard pending schema
|
|
1020
|
+
stats XP, level, streaks
|
|
1021
|
+
achievements Achievement list
|
|
1022
|
+
map [--ai] Dungeon map view
|
|
875
1023
|
|
|
876
|
-
|
|
1024
|
+
Configuration:
|
|
877
1025
|
init Setup wizard
|
|
878
1026
|
config Show settings
|
|
1027
|
+
config -k=<key> Set API key
|
|
1028
|
+
config -m=<model> Set model
|
|
1029
|
+
config -t=<theme> Set theme (fantasy, space, starwars, etc)
|
|
1030
|
+
config -r="<rules>" Custom AI rules
|
|
1031
|
+
|
|
1032
|
+
Themes:
|
|
1033
|
+
default, fantasy, space, starwars, western, cyberpunk, pirate
|
|
1034
|
+
|
|
1035
|
+
File Operations:
|
|
1036
|
+
cp, mv, rm [-rf] Standard file operations
|
|
879
1037
|
clean --yes Clear current folder
|
|
880
|
-
v, version Show version
|
|
881
|
-
help This help
|
|
882
|
-
exit, quit Exit
|
|
883
1038
|
|
|
884
|
-
|
|
885
|
-
<
|
|
886
|
-
|
|
1039
|
+
AI Generation:
|
|
1040
|
+
<description> AI generates preview
|
|
1041
|
+
save Save to folders/file
|
|
1042
|
+
cancel Discard
|
|
887
1043
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
deadline +3d Due in 3 days
|
|
892
|
-
deadline Jan 15 Due on date
|
|
1044
|
+
Clipboard:
|
|
1045
|
+
<cmd> | pbcopy Copy to clipboard (macOS)
|
|
1046
|
+
<cmd> | clip Copy to clipboard (Windows)
|
|
893
1047
|
|
|
894
1048
|
www.rlc.rocks
|
|
895
1049
|
`.trim()
|
|
@@ -19,16 +19,16 @@ const ASCII_ART = [
|
|
|
19
19
|
'║ Roguelike CLI ║',
|
|
20
20
|
'╚═════════════════════════╝',
|
|
21
21
|
'',
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
22
|
+
' Tasks: done, undo, deadline, boss, block',
|
|
23
|
+
' Stats: stats, achievements, map, check',
|
|
24
|
+
' Config: init, config -t=<theme>',
|
|
25
25
|
'',
|
|
26
|
-
'
|
|
27
|
-
' Workflow: <description> -> refine -> save',
|
|
26
|
+
' Themes: fantasy, space, starwars, cyberpunk',
|
|
28
27
|
'',
|
|
29
|
-
'
|
|
28
|
+
' TAB autocomplete, | pbcopy to copy',
|
|
29
|
+
' <description> -> refine -> save',
|
|
30
30
|
'',
|
|
31
|
-
' www.rlc.rocks',
|
|
31
|
+
' help - commands, www.rlc.rocks',
|
|
32
32
|
'',
|
|
33
33
|
' Ready...',
|
|
34
34
|
'',
|
package/dist/storage/profile.js
CHANGED
|
@@ -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
package/src/ai/claude.ts
CHANGED
|
@@ -60,7 +60,7 @@ export async function generateSchemaWithAI(
|
|
|
60
60
|
history?: ConversationMessage[]
|
|
61
61
|
): Promise<GeneratedSchema | null> {
|
|
62
62
|
if (!config.apiKey) {
|
|
63
|
-
throw new Error('API key not set. Use config
|
|
63
|
+
throw new Error('API key not set. Use config -k=<key> to set it.');
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const client = new Anthropic({
|
|
@@ -87,10 +87,17 @@ export async function generateSchemaWithAI(
|
|
|
87
87
|
|
|
88
88
|
try {
|
|
89
89
|
const model = config.model || 'claude-sonnet-4-20250514';
|
|
90
|
+
|
|
91
|
+
// Build system prompt with custom rules
|
|
92
|
+
let systemPrompt = SYSTEM_PROMPT;
|
|
93
|
+
if (config.rules) {
|
|
94
|
+
systemPrompt += '\n\nADDITIONAL STYLE RULES (apply to all responses):\n' + config.rules;
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
const message = await client.messages.create({
|
|
91
98
|
model: model,
|
|
92
99
|
max_tokens: 2000,
|
|
93
|
-
system:
|
|
100
|
+
system: systemPrompt,
|
|
94
101
|
messages: messages,
|
|
95
102
|
});
|
|
96
103
|
|
|
@@ -126,7 +133,7 @@ export async function generateDungeonMapWithAI(
|
|
|
126
133
|
signal?: AbortSignal
|
|
127
134
|
): Promise<string | null> {
|
|
128
135
|
if (!config.apiKey) {
|
|
129
|
-
throw new Error('API key not set. Use config
|
|
136
|
+
throw new Error('API key not set. Use config -k=<key> to set it.');
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
const client = new Anthropic({
|
|
@@ -135,10 +142,17 @@ export async function generateDungeonMapWithAI(
|
|
|
135
142
|
|
|
136
143
|
try {
|
|
137
144
|
const model = config.model || 'claude-sonnet-4-20250514';
|
|
145
|
+
|
|
146
|
+
// Build system prompt with custom rules
|
|
147
|
+
let systemPrompt = DUNGEON_MAP_PROMPT;
|
|
148
|
+
if (config.rules) {
|
|
149
|
+
systemPrompt += '\n\nADDITIONAL STYLE RULES:\n' + config.rules;
|
|
150
|
+
}
|
|
151
|
+
|
|
138
152
|
const message = await client.messages.create({
|
|
139
153
|
model: model,
|
|
140
154
|
max_tokens: 2000,
|
|
141
|
-
system:
|
|
155
|
+
system: systemPrompt,
|
|
142
156
|
messages: [{
|
|
143
157
|
role: 'user',
|
|
144
158
|
content: 'Generate a dungeon map for this task tree:\n\n' + treeContent
|
package/src/commands/init.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import * as readline from 'readline';
|
|
5
|
-
import { Config, saveConfig, initConfig } from '../config/config';
|
|
5
|
+
import { Config, saveConfig, initConfig, RULES_PRESETS } from '../config/config';
|
|
6
6
|
|
|
7
7
|
function question(rl: readline.Interface, query: string): Promise<string> {
|
|
8
8
|
return new Promise((resolve) => {
|
|
@@ -31,7 +31,6 @@ function copyRecursive(src: string, dest: string): void {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export async function initCommand(existingRl?: readline.Interface): Promise<void> {
|
|
34
|
-
// Create our own readline if not provided, or use existing one
|
|
35
34
|
const rl = existingRl || readline.createInterface({
|
|
36
35
|
input: process.stdin,
|
|
37
36
|
output: process.stdout,
|
|
@@ -44,7 +43,6 @@ export async function initCommand(existingRl?: readline.Interface): Promise<void
|
|
|
44
43
|
console.log('║ ROGUELIKE CLI INITIALIZATION WIZARD ║');
|
|
45
44
|
console.log('╚═══════════════════════════════════════╝\n');
|
|
46
45
|
|
|
47
|
-
// Get existing config if any
|
|
48
46
|
const existingConfig = await initConfig();
|
|
49
47
|
const oldStoragePath = existingConfig?.storagePath;
|
|
50
48
|
|
|
@@ -101,7 +99,7 @@ export async function initCommand(existingRl?: readline.Interface): Promise<void
|
|
|
101
99
|
|
|
102
100
|
console.log(`Selected: ${selectedProvider.name} (${selectedProvider.model})`);
|
|
103
101
|
|
|
104
|
-
// 3. API Key
|
|
102
|
+
// 3. API Key
|
|
105
103
|
const existingApiKey = existingConfig?.apiKey || '';
|
|
106
104
|
const hasExistingKey = existingApiKey.length > 0;
|
|
107
105
|
const keyPrompt = hasExistingKey
|
|
@@ -112,13 +110,49 @@ export async function initCommand(existingRl?: readline.Interface): Promise<void
|
|
|
112
110
|
const apiKey = apiKeyInput.trim() || existingApiKey;
|
|
113
111
|
|
|
114
112
|
if (!apiKey) {
|
|
115
|
-
console.log('Warning: API key not set. You can set it later with config
|
|
113
|
+
console.log('Warning: API key not set. You can set it later with: config -k <key>');
|
|
116
114
|
} else if (apiKeyInput.trim()) {
|
|
117
115
|
console.log('API key saved');
|
|
118
116
|
} else {
|
|
119
117
|
console.log('Using existing API key');
|
|
120
118
|
}
|
|
121
119
|
|
|
120
|
+
// 4. Theme/Rules selection
|
|
121
|
+
console.log('\nSelect AI Theme (affects language style):');
|
|
122
|
+
const presetKeys = Object.keys(RULES_PRESETS);
|
|
123
|
+
presetKeys.forEach((key, index) => {
|
|
124
|
+
console.log(` ${index + 1}. ${RULES_PRESETS[key].name}`);
|
|
125
|
+
});
|
|
126
|
+
console.log(` ${presetKeys.length + 1}. Custom (enter your own rules)`);
|
|
127
|
+
|
|
128
|
+
const existingPreset = existingConfig?.rulesPreset || 'default';
|
|
129
|
+
const defaultPresetIndex = presetKeys.indexOf(existingPreset) + 1 || 1;
|
|
130
|
+
|
|
131
|
+
const themeChoice = await question(rl, `\nEnter choice [1-${presetKeys.length + 1}] (default: ${defaultPresetIndex}): `);
|
|
132
|
+
const themeIndex = parseInt(themeChoice.trim()) - 1;
|
|
133
|
+
|
|
134
|
+
let selectedRules = '';
|
|
135
|
+
let selectedPreset = 'default';
|
|
136
|
+
|
|
137
|
+
if (themeIndex >= 0 && themeIndex < presetKeys.length) {
|
|
138
|
+
selectedPreset = presetKeys[themeIndex];
|
|
139
|
+
selectedRules = RULES_PRESETS[selectedPreset].rules;
|
|
140
|
+
console.log(`Selected: ${RULES_PRESETS[selectedPreset].name}`);
|
|
141
|
+
} else if (themeIndex === presetKeys.length) {
|
|
142
|
+
// Custom rules
|
|
143
|
+
console.log('\nEnter your custom rules for AI (how it should speak, what terms to use):');
|
|
144
|
+
console.log('Example: "Use pirate language. Tasks are treasure hunts. Be playful."');
|
|
145
|
+
const customRules = await question(rl, '\nYour rules: ');
|
|
146
|
+
selectedRules = customRules.trim();
|
|
147
|
+
selectedPreset = 'custom';
|
|
148
|
+
console.log('Custom rules saved');
|
|
149
|
+
} else {
|
|
150
|
+
// Default
|
|
151
|
+
selectedPreset = existingConfig?.rulesPreset || 'default';
|
|
152
|
+
selectedRules = existingConfig?.rules || '';
|
|
153
|
+
console.log(`Keeping: ${RULES_PRESETS[selectedPreset]?.name || 'Default'}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
122
156
|
// Save config
|
|
123
157
|
const config: Config = {
|
|
124
158
|
aiProvider: selectedProvider.name as any,
|
|
@@ -127,11 +161,12 @@ export async function initCommand(existingRl?: readline.Interface): Promise<void
|
|
|
127
161
|
storagePath: rootDir,
|
|
128
162
|
currentPath: rootDir,
|
|
129
163
|
model: selectedProvider.model,
|
|
164
|
+
rules: selectedRules,
|
|
165
|
+
rulesPreset: selectedPreset,
|
|
130
166
|
};
|
|
131
167
|
|
|
132
168
|
saveConfig(config);
|
|
133
169
|
|
|
134
|
-
// Ensure storage directory exists
|
|
135
170
|
if (!fs.existsSync(rootDir)) {
|
|
136
171
|
fs.mkdirSync(rootDir, { recursive: true });
|
|
137
172
|
}
|
|
@@ -141,7 +176,8 @@ export async function initCommand(existingRl?: readline.Interface): Promise<void
|
|
|
141
176
|
console.log('╚═══════════════════════════════════════╝\n');
|
|
142
177
|
console.log(`Root directory: ${rootDir}`);
|
|
143
178
|
console.log(`AI Provider: ${selectedProvider.name}`);
|
|
144
|
-
console.log(`Model: ${selectedProvider.model}
|
|
179
|
+
console.log(`Model: ${selectedProvider.model}`);
|
|
180
|
+
console.log(`Theme: ${RULES_PRESETS[selectedPreset]?.name || 'Custom'}\n`);
|
|
145
181
|
} finally {
|
|
146
182
|
if (shouldCloseRl) {
|
|
147
183
|
rl.close();
|
package/src/config/config.ts
CHANGED
|
@@ -9,8 +9,42 @@ export interface Config {
|
|
|
9
9
|
storagePath: string;
|
|
10
10
|
currentPath: string;
|
|
11
11
|
model?: string;
|
|
12
|
+
rules?: string;
|
|
13
|
+
rulesPreset?: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
// Preset rules for different themes
|
|
17
|
+
export const RULES_PRESETS: Record<string, { name: string; rules: string }> = {
|
|
18
|
+
default: {
|
|
19
|
+
name: 'Default (No theme)',
|
|
20
|
+
rules: '',
|
|
21
|
+
},
|
|
22
|
+
fantasy: {
|
|
23
|
+
name: 'Fantasy RPG',
|
|
24
|
+
rules: 'Use fantasy RPG language. Tasks are "quests", completing them is "slaying". Major milestones are "boss battles". Use terms like "adventurer", "dungeon", "loot", "guild". Add flavor text with swords, dragons, magic.',
|
|
25
|
+
},
|
|
26
|
+
space: {
|
|
27
|
+
name: 'Space Opera',
|
|
28
|
+
rules: 'Use sci-fi space language. Tasks are "missions", completing them is "mission accomplished". Major milestones are "final frontier". Use terms like "commander", "starship", "coordinates", "hyperdrive". Add flavor with stars, planets, aliens.',
|
|
29
|
+
},
|
|
30
|
+
starwars: {
|
|
31
|
+
name: 'Star Wars',
|
|
32
|
+
rules: 'Use Star Wars language. Tasks are "missions from the Rebel Alliance". Completing is "defeating the Empire". Milestones are "destroying the Death Star". Use "Jedi", "Force", "Padawan", "Master". May the Force be with you.',
|
|
33
|
+
},
|
|
34
|
+
western: {
|
|
35
|
+
name: 'Wild West',
|
|
36
|
+
rules: 'Use Wild West language. Tasks are "bounties", completing them is "collecting the reward". Milestones are "showdowns". Use terms like "sheriff", "outlaw", "saloon", "frontier", "partner". Add dusty trails and tumbleweeds.',
|
|
37
|
+
},
|
|
38
|
+
cyberpunk: {
|
|
39
|
+
name: 'Cyberpunk',
|
|
40
|
+
rules: 'Use cyberpunk language. Tasks are "gigs", completing them is "flatlined". Milestones are "megacorp takedowns". Use terms like "netrunner", "chrome", "corpo", "edgerunner", "eddies". Add neon and rain.',
|
|
41
|
+
},
|
|
42
|
+
pirate: {
|
|
43
|
+
name: 'Pirate',
|
|
44
|
+
rules: 'Use pirate language. Tasks are "plunder", completing them is "claiming the treasure". Milestones are "capturing the flagship". Use "captain", "crew", "booty", "seven seas", "landlubber". Arr matey!',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
14
48
|
const CONFIG_FILE = path.join(os.homedir(), '.rlc', 'config.json');
|
|
15
49
|
const DEFAULT_STORAGE = path.join(os.homedir(), '.rlc', 'workspace');
|
|
16
50
|
|