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.
@@ -19,11 +19,14 @@ const ASCII_ART = [
19
19
  '║ Roguelike CLI ║',
20
20
  '╚═════════════════════════╝',
21
21
  '',
22
- ' Commands: ls, cd, mkdir, open, cp, mv, rm, tree, pwd, clean',
23
- ' TAB to autocomplete, | pbcopy to copy output',
22
+ ' Navigation: ls, cd, tree, pwd, open',
23
+ ' Tasks: done, deadline, boss, block, status',
24
+ ' Gamification: stats, achievements, map',
24
25
  '',
26
+ ' TAB to autocomplete, | pbcopy to copy',
25
27
  ' Workflow: <description> -> refine -> save',
26
- ' init - setup, config - settings, help - examples',
28
+ '',
29
+ ' help - all commands, init - setup',
27
30
  '',
28
31
  ' www.rlc.rocks',
29
32
  '',
@@ -36,7 +36,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.readNodeConfig = readNodeConfig;
37
37
  exports.writeNodeConfig = writeNodeConfig;
38
38
  exports.createNode = createNode;
39
+ exports.calculateXP = calculateXP;
39
40
  exports.saveSchemaFile = saveSchemaFile;
41
+ exports.saveMapFile = saveMapFile;
40
42
  exports.readSchemaFile = readSchemaFile;
41
43
  const fs = __importStar(require("fs"));
42
44
  const path = __importStar(require("path"));
@@ -48,7 +50,13 @@ function readNodeConfig(nodePath) {
48
50
  }
49
51
  try {
50
52
  const data = fs.readFileSync(configPath, 'utf-8');
51
- return JSON.parse(data);
53
+ const config = JSON.parse(data);
54
+ // Ensure defaults for older configs
55
+ return {
56
+ status: 'open',
57
+ xp: 10,
58
+ ...config,
59
+ };
52
60
  }
53
61
  catch {
54
62
  return null;
@@ -61,6 +69,9 @@ function writeNodeConfig(nodePath, config) {
61
69
  const configPath = path.join(nodePath, CONFIG_FILE);
62
70
  const existing = readNodeConfig(nodePath);
63
71
  const updated = {
72
+ name: config.name || existing?.name || path.basename(nodePath),
73
+ status: config.status ?? existing?.status ?? 'open',
74
+ xp: config.xp ?? existing?.xp ?? 10,
64
75
  ...existing,
65
76
  ...config,
66
77
  updatedAt: new Date().toISOString(),
@@ -76,7 +87,10 @@ function createNode(parentPath, name, options) {
76
87
  const nodePath = path.join(parentPath, safeName);
77
88
  const config = {
78
89
  name,
90
+ status: 'open',
91
+ xp: options?.xp || 10,
79
92
  deadline: options?.deadline,
93
+ isBoss: options?.isBoss,
80
94
  branch: options?.branch,
81
95
  zone: options?.zone,
82
96
  description: options?.description,
@@ -87,6 +101,13 @@ function createNode(parentPath, name, options) {
87
101
  writeNodeConfig(nodePath, config);
88
102
  return nodePath;
89
103
  }
104
+ // Calculate XP based on depth (deeper = more XP)
105
+ function calculateXP(depth, isBoss = false) {
106
+ const baseXP = 10;
107
+ const depthBonus = depth * 5;
108
+ const bossMultiplier = isBoss ? 3 : 1;
109
+ return (baseXP + depthBonus) * bossMultiplier;
110
+ }
90
111
  // Save schema content to .rlc.schema file
91
112
  function saveSchemaFile(dirPath, filename, content) {
92
113
  if (!fs.existsSync(dirPath)) {
@@ -100,6 +121,19 @@ function saveSchemaFile(dirPath, filename, content) {
100
121
  fs.writeFileSync(schemaPath, content, 'utf-8');
101
122
  return schemaPath;
102
123
  }
124
+ // Save map content to .rlc.map file
125
+ function saveMapFile(dirPath, filename, content) {
126
+ if (!fs.existsSync(dirPath)) {
127
+ fs.mkdirSync(dirPath, { recursive: true });
128
+ }
129
+ const safeName = filename
130
+ .toLowerCase()
131
+ .replace(/[^a-z0-9]+/g, '-')
132
+ .replace(/^-+|-+$/g, '');
133
+ const mapPath = path.join(dirPath, `${safeName}.rlc.map`);
134
+ fs.writeFileSync(mapPath, content, 'utf-8');
135
+ return mapPath;
136
+ }
103
137
  // Read schema file
104
138
  function readSchemaFile(filePath) {
105
139
  if (!fs.existsSync(filePath)) {
@@ -0,0 +1,276 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ACHIEVEMENTS = void 0;
37
+ exports.xpForLevel = xpForLevel;
38
+ exports.levelFromXP = levelFromXP;
39
+ exports.xpToNextLevel = xpToNextLevel;
40
+ exports.readProfile = readProfile;
41
+ exports.saveProfile = saveProfile;
42
+ exports.addXP = addXP;
43
+ exports.completeTask = completeTask;
44
+ exports.formatStats = formatStats;
45
+ exports.formatAchievements = formatAchievements;
46
+ const fs = __importStar(require("fs"));
47
+ const path = __importStar(require("path"));
48
+ const os = __importStar(require("os"));
49
+ const PROFILE_FILE = path.join(os.homedir(), '.rlc', 'profile.json');
50
+ // Achievement definitions
51
+ exports.ACHIEVEMENTS = [
52
+ { id: 'first_blood', name: 'First Blood', description: 'Complete your first task' },
53
+ { id: 'ten_tasks', name: 'Getting Started', description: 'Complete 10 tasks' },
54
+ { id: 'fifty_tasks', name: 'Productive', description: 'Complete 50 tasks' },
55
+ { id: 'hundred_tasks', name: 'Centurion', description: 'Complete 100 tasks' },
56
+ { id: 'deep_nesting', name: 'Deep Diver', description: 'Complete a task at depth 5+' },
57
+ { id: 'boss_slayer', name: 'Boss Slayer', description: 'Complete a boss task' },
58
+ { id: 'five_bosses', name: 'Boss Hunter', description: 'Defeat 5 bosses' },
59
+ { id: 'speedrunner', name: 'Speedrunner', description: 'Complete a task on the same day it was created' },
60
+ { id: 'streak_3', name: 'On a Roll', description: '3 day completion streak' },
61
+ { id: 'streak_7', name: 'Streak Master', description: '7 day completion streak' },
62
+ { id: 'streak_30', name: 'Unstoppable', description: '30 day completion streak' },
63
+ { id: 'level_5', name: 'Adventurer', description: 'Reach level 5' },
64
+ { id: 'level_10', name: 'Veteran', description: 'Reach level 10' },
65
+ { id: 'level_25', name: 'Legend', description: 'Reach level 25' },
66
+ { id: 'xp_1000', name: 'XP Collector', description: 'Earn 1000 XP' },
67
+ { id: 'xp_10000', name: 'XP Hoarder', description: 'Earn 10000 XP' },
68
+ ];
69
+ // XP required for each level (cumulative)
70
+ function xpForLevel(level) {
71
+ return Math.floor(100 * Math.pow(1.5, level - 1));
72
+ }
73
+ function levelFromXP(xp) {
74
+ let level = 1;
75
+ let totalRequired = 0;
76
+ while (totalRequired + xpForLevel(level) <= xp) {
77
+ totalRequired += xpForLevel(level);
78
+ level++;
79
+ }
80
+ return level;
81
+ }
82
+ function xpToNextLevel(xp) {
83
+ const level = levelFromXP(xp);
84
+ let totalForCurrentLevel = 0;
85
+ for (let i = 1; i < level; i++) {
86
+ totalForCurrentLevel += xpForLevel(i);
87
+ }
88
+ const xpInCurrentLevel = xp - totalForCurrentLevel;
89
+ const required = xpForLevel(level);
90
+ return {
91
+ current: xpInCurrentLevel,
92
+ required,
93
+ progress: Math.floor((xpInCurrentLevel / required) * 100),
94
+ };
95
+ }
96
+ function readProfile() {
97
+ if (!fs.existsSync(PROFILE_FILE)) {
98
+ return createDefaultProfile();
99
+ }
100
+ try {
101
+ const data = fs.readFileSync(PROFILE_FILE, 'utf-8');
102
+ const profile = JSON.parse(data);
103
+ return {
104
+ ...createDefaultProfile(),
105
+ ...profile,
106
+ };
107
+ }
108
+ catch {
109
+ return createDefaultProfile();
110
+ }
111
+ }
112
+ function saveProfile(profile) {
113
+ const dir = path.dirname(PROFILE_FILE);
114
+ if (!fs.existsSync(dir)) {
115
+ fs.mkdirSync(dir, { recursive: true });
116
+ }
117
+ // Update level based on XP
118
+ profile.level = levelFromXP(profile.totalXP);
119
+ fs.writeFileSync(PROFILE_FILE, JSON.stringify(profile, null, 2), 'utf-8');
120
+ }
121
+ function createDefaultProfile() {
122
+ return {
123
+ totalXP: 0,
124
+ level: 1,
125
+ tasksCompleted: 0,
126
+ bossesDefeated: 0,
127
+ currentStreak: 0,
128
+ longestStreak: 0,
129
+ achievements: [],
130
+ stats: {
131
+ completedByDay: {},
132
+ createdAt: new Date().toISOString(),
133
+ },
134
+ };
135
+ }
136
+ function addXP(amount) {
137
+ const profile = readProfile();
138
+ const oldLevel = profile.level;
139
+ profile.totalXP += amount;
140
+ profile.level = levelFromXP(profile.totalXP);
141
+ saveProfile(profile);
142
+ return {
143
+ newXP: profile.totalXP,
144
+ levelUp: profile.level > oldLevel,
145
+ newLevel: profile.level,
146
+ };
147
+ }
148
+ function completeTask(xp, isBoss, depth, createdAt) {
149
+ const profile = readProfile();
150
+ const oldLevel = profile.level;
151
+ const today = new Date().toISOString().split('T')[0];
152
+ const createdDate = createdAt.split('T')[0];
153
+ // Add XP
154
+ profile.totalXP += xp;
155
+ profile.tasksCompleted += 1;
156
+ if (isBoss) {
157
+ profile.bossesDefeated += 1;
158
+ }
159
+ // Update streak
160
+ if (profile.lastCompletionDate) {
161
+ const lastDate = new Date(profile.lastCompletionDate);
162
+ const todayDate = new Date(today);
163
+ const diffDays = Math.floor((todayDate.getTime() - lastDate.getTime()) / (1000 * 60 * 60 * 24));
164
+ if (diffDays === 1) {
165
+ profile.currentStreak += 1;
166
+ }
167
+ else if (diffDays > 1) {
168
+ profile.currentStreak = 1;
169
+ }
170
+ // Same day - streak continues but doesn't increment
171
+ }
172
+ else {
173
+ profile.currentStreak = 1;
174
+ }
175
+ profile.longestStreak = Math.max(profile.longestStreak, profile.currentStreak);
176
+ profile.lastCompletionDate = today;
177
+ // Update daily stats
178
+ profile.stats.completedByDay[today] = (profile.stats.completedByDay[today] || 0) + 1;
179
+ // Update level
180
+ profile.level = levelFromXP(profile.totalXP);
181
+ // Check for new achievements
182
+ const newAchievements = [];
183
+ const checkAchievement = (id) => {
184
+ if (!profile.achievements.includes(id)) {
185
+ profile.achievements.push(id);
186
+ const achievement = exports.ACHIEVEMENTS.find(a => a.id === id);
187
+ if (achievement)
188
+ newAchievements.push(achievement);
189
+ }
190
+ };
191
+ // Task count achievements
192
+ if (profile.tasksCompleted >= 1)
193
+ checkAchievement('first_blood');
194
+ if (profile.tasksCompleted >= 10)
195
+ checkAchievement('ten_tasks');
196
+ if (profile.tasksCompleted >= 50)
197
+ checkAchievement('fifty_tasks');
198
+ if (profile.tasksCompleted >= 100)
199
+ checkAchievement('hundred_tasks');
200
+ // Boss achievements
201
+ if (isBoss)
202
+ checkAchievement('boss_slayer');
203
+ if (profile.bossesDefeated >= 5)
204
+ checkAchievement('five_bosses');
205
+ // Depth achievement
206
+ if (depth >= 5)
207
+ checkAchievement('deep_nesting');
208
+ // Speedrunner achievement
209
+ if (createdDate === today)
210
+ checkAchievement('speedrunner');
211
+ // Streak achievements
212
+ if (profile.currentStreak >= 3)
213
+ checkAchievement('streak_3');
214
+ if (profile.currentStreak >= 7)
215
+ checkAchievement('streak_7');
216
+ if (profile.currentStreak >= 30)
217
+ checkAchievement('streak_30');
218
+ // Level achievements
219
+ if (profile.level >= 5)
220
+ checkAchievement('level_5');
221
+ if (profile.level >= 10)
222
+ checkAchievement('level_10');
223
+ if (profile.level >= 25)
224
+ checkAchievement('level_25');
225
+ // XP achievements
226
+ if (profile.totalXP >= 1000)
227
+ checkAchievement('xp_1000');
228
+ if (profile.totalXP >= 10000)
229
+ checkAchievement('xp_10000');
230
+ saveProfile(profile);
231
+ return {
232
+ xpGained: xp,
233
+ levelUp: profile.level > oldLevel,
234
+ newLevel: profile.level,
235
+ newAchievements,
236
+ };
237
+ }
238
+ function formatStats() {
239
+ const profile = readProfile();
240
+ const nextLevel = xpToNextLevel(profile.totalXP);
241
+ const lines = [
242
+ '',
243
+ '=== PLAYER STATS ===',
244
+ '',
245
+ `Level: ${profile.level}`,
246
+ `XP: ${profile.totalXP} (${nextLevel.current}/${nextLevel.required} to next level)`,
247
+ `Progress: [${'#'.repeat(Math.floor(nextLevel.progress / 5))}${'.'.repeat(20 - Math.floor(nextLevel.progress / 5))}] ${nextLevel.progress}%`,
248
+ '',
249
+ `Tasks Completed: ${profile.tasksCompleted}`,
250
+ `Bosses Defeated: ${profile.bossesDefeated}`,
251
+ `Current Streak: ${profile.currentStreak} days`,
252
+ `Longest Streak: ${profile.longestStreak} days`,
253
+ '',
254
+ `Achievements: ${profile.achievements.length}/${exports.ACHIEVEMENTS.length}`,
255
+ '',
256
+ ];
257
+ return lines.join('\n');
258
+ }
259
+ function formatAchievements() {
260
+ const profile = readProfile();
261
+ const lines = [
262
+ '',
263
+ '=== ACHIEVEMENTS ===',
264
+ '',
265
+ ];
266
+ for (const achievement of exports.ACHIEVEMENTS) {
267
+ const unlocked = profile.achievements.includes(achievement.id);
268
+ const status = unlocked ? '[x]' : '[ ]';
269
+ lines.push(`${status} ${achievement.name}`);
270
+ lines.push(` ${achievement.description}`);
271
+ }
272
+ lines.push('');
273
+ lines.push(`Unlocked: ${profile.achievements.length}/${exports.ACHIEVEMENTS.length}`);
274
+ lines.push('');
275
+ return lines.join('\n');
276
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roguelike-cli",
3
- "version": "1.2.5",
3
+ "version": "1.3.0",
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
@@ -16,49 +16,19 @@ export interface ConversationMessage {
16
16
  const SYSTEM_PROMPT = `You are a schema generator. Based on user input, generate EITHER:
17
17
 
18
18
  1. **BLOCK DIAGRAM** - when user mentions: "schema", "architecture", "infrastructure", "diagram", "system"
19
- Use box-drawing to create visual blocks with connections:
20
-
21
- Example:
22
- \`\`\`
23
- ┌─────────────────────────────────────────────────────────────┐
24
- │ Kubernetes Cluster │
25
- │ │
26
- │ ┌──────────────────┐ ┌──────────────────┐ │
27
- │ │ Control Plane │ │ Worker Nodes │ │
28
- │ │ │◄────►│ │ │
29
- │ │ - API Server │ │ - Node Pool 1 │ │
30
- │ │ - Scheduler │ │ - Node Pool 2 │ │
31
- │ │ - etcd │ │ - GPU Pool │ │
32
- │ └────────┬─────────┘ └────────┬─────────┘ │
33
- │ │ │ │
34
- │ └──────────┬───────────────┘ │
35
- │ │ │
36
- │ ┌──────────────────┐│┌──────────────────┐ │
37
- │ │ PostgreSQL │││ Redis │ │
38
- │ └──────────────────┘│└──────────────────┘ │
39
- └─────────────────────────────────────────────────────────────┘
40
- \`\`\`
19
+ Use box-drawing to create visual blocks with connections.
41
20
 
42
21
  2. **TREE STRUCTURE** - when user mentions: "todo", "tasks", "list", "steps", "plan"
43
- Use tree format:
44
-
45
- Example:
46
- \`\`\`
47
- ├── Phase 1: Setup
48
- │ ├── Create repository
49
- │ ├── Setup CI/CD
50
- │ └── Configure environment
51
- ├── Phase 2: Development
52
- │ ├── Backend API
53
- │ └── Frontend UI
54
- └── Phase 3: Deploy
55
- \`\`\`
22
+ Use tree format with metadata tags:
23
+ - [BOSS] or [MILESTONE] for major milestones
24
+ - [DUE: date] for deadlines (today, tomorrow, +3d, Jan 15)
56
25
 
57
26
  Rules:
58
27
  1. Extract a short title for filename
59
- 2. If user says "schema" or "architecture" - ALWAYS use BLOCK DIAGRAM format
28
+ 2. If user says "schema" or "architecture" - use BLOCK DIAGRAM format
60
29
  3. If user says "todo" or "tasks" - use TREE format
61
30
  4. Keep context from previous messages
31
+ 5. For todos: add [BOSS] tags for major milestones, suggest deadlines
62
32
 
63
33
  Respond with JSON:
64
34
  {
@@ -67,6 +37,22 @@ Respond with JSON:
67
37
  "content": "the actual ASCII art schema here"
68
38
  }`;
69
39
 
40
+ const DUNGEON_MAP_PROMPT = `You are a dungeon map generator for a roguelike task manager.
41
+ Given a tree structure of tasks, create an ASCII dungeon map where:
42
+ - Each major task group is a ROOM
43
+ - Sub-tasks are items inside rooms (marked with *)
44
+ - Boss/milestone tasks [BOSS] are marked with @ symbol
45
+ - Completed tasks [DONE] are marked with x
46
+ - Blocked tasks [BLOCKED] are marked with !
47
+ - Rooms are connected by corridors (|, +, -)
48
+ - Use # for walls
49
+ - Use + for doors between rooms
50
+ - Be creative with room shapes and layouts
51
+ - Include a legend at the bottom
52
+
53
+ Create a creative, interesting dungeon layout for the given tasks.
54
+ Output ONLY the ASCII map, no JSON wrapper.`;
55
+
70
56
  export async function generateSchemaWithAI(
71
57
  input: string,
72
58
  config: Config,
@@ -81,22 +67,19 @@ export async function generateSchemaWithAI(
81
67
  apiKey: config.apiKey,
82
68
  });
83
69
 
84
- // Build messages from history or just the current input
85
70
  const messages: { role: 'user' | 'assistant'; content: string }[] = [];
86
71
 
87
72
  if (history && history.length > 0) {
88
- // Add previous messages for context
89
- for (const msg of history.slice(0, -1)) { // exclude the last one (current input)
73
+ for (const msg of history.slice(0, -1)) {
90
74
  messages.push({
91
75
  role: msg.role,
92
76
  content: msg.role === 'assistant'
93
- ? `Previous schema generated:\n${msg.content}`
77
+ ? 'Previous schema generated:\n' + msg.content
94
78
  : msg.content
95
79
  });
96
80
  }
97
81
  }
98
82
 
99
- // Add current user input
100
83
  messages.push({
101
84
  role: 'user',
102
85
  content: input
@@ -124,8 +107,6 @@ export async function generateSchemaWithAI(
124
107
  }
125
108
 
126
109
  const parsed = JSON.parse(jsonMatch[0]);
127
-
128
- // AI now returns ready content
129
110
  const schemaContent = parsed.content || '';
130
111
 
131
112
  return {
@@ -139,3 +120,39 @@ export async function generateSchemaWithAI(
139
120
  }
140
121
  }
141
122
 
123
+ export async function generateDungeonMapWithAI(
124
+ treeContent: string,
125
+ config: Config,
126
+ signal?: AbortSignal
127
+ ): Promise<string | null> {
128
+ if (!config.apiKey) {
129
+ throw new Error('API key not set. Use config:apiKey=<key> to set it.');
130
+ }
131
+
132
+ const client = new Anthropic({
133
+ apiKey: config.apiKey,
134
+ });
135
+
136
+ try {
137
+ const model = config.model || 'claude-sonnet-4-20250514';
138
+ const message = await client.messages.create({
139
+ model: model,
140
+ max_tokens: 2000,
141
+ system: DUNGEON_MAP_PROMPT,
142
+ messages: [{
143
+ role: 'user',
144
+ content: 'Generate a dungeon map for this task tree:\n\n' + treeContent
145
+ }],
146
+ });
147
+
148
+ const content = message.content[0];
149
+ if (content.type !== 'text') {
150
+ return null;
151
+ }
152
+
153
+ return content.text.trim();
154
+ } catch (error: any) {
155
+ console.error('AI Error:', error.message);
156
+ return null;
157
+ }
158
+ }