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.
@@ -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') });
@@ -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 Show task tree with status
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 Mark current task as completed (recursive)
853
- deadline <date> Set deadline (today, tomorrow, +3d, Jan 15)
854
- boss Toggle boss/milestone status (3x XP)
855
- block [reason] Mark task as blocked
856
- unblock Remove blocked status
857
- status Show current task details
858
-
859
- File Operations:
860
- cp <src> <dest> Copy file or folder
861
- mv <src> <dest> Move/rename
862
- rm <name> Delete file
863
- rm -rf <name> Delete folder
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 Show XP, level, streaks
867
- achievements Show achievement list
868
- map Dungeon map view
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
- Utility:
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
- Clipboard:
885
- <cmd> | pbcopy Copy output (macOS)
886
- <cmd> | clip Copy output (Windows)
1039
+ AI Generation:
1040
+ <description> AI generates preview
1041
+ save Save to folders/file
1042
+ cancel Discard
887
1043
 
888
- Deadlines:
889
- deadline today Due today
890
- deadline tomorrow Due tomorrow
891
- deadline +3d Due in 3 days
892
- deadline Jan 15 Due on date
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
- ' Navigation: ls, cd, tree, pwd, open',
23
- ' Tasks: done, deadline, boss, block, status',
24
- ' Gamification: stats, achievements, map',
22
+ ' Tasks: done, undo, deadline, boss, block',
23
+ ' Stats: stats, achievements, map, check',
24
+ ' Config: init, config -t=<theme>',
25
25
  '',
26
- ' TAB to autocomplete, | pbcopy to copy',
27
- ' Workflow: <description> -> refine -> save',
26
+ ' Themes: fantasy, space, starwars, cyberpunk',
28
27
  '',
29
- ' help - all commands, init - setup',
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
  '',
@@ -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.2",
4
4
  "description": "AI-powered interactive terminal for creating schemas and todo lists",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
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:apiKey=<key> to set it.');
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: SYSTEM_PROMPT,
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:apiKey=<key> to set it.');
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: DUNGEON_MAP_PROMPT,
155
+ system: systemPrompt,
142
156
  messages: [{
143
157
  role: 'user',
144
158
  content: 'Generate a dungeon map for this task tree:\n\n' + treeContent
@@ -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 - reuse existing if not provided
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:apiKey=<key>');
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}\n`);
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();
@@ -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