roguelike-cli 1.2.6 → 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.
@@ -4,40 +4,38 @@ import * as readline from 'readline';
4
4
  import { execSync } from 'child_process';
5
5
  import { Config } from '../config/config';
6
6
  import { listSchemas, navigateToNode, getTree } from '../storage/storage';
7
- import { saveSchemaFile, writeNodeConfig } from '../storage/nodeConfig';
8
- import { generateSchemaWithAI } from '../ai/claude';
7
+ import { saveSchemaFile, writeNodeConfig, readNodeConfig, calculateXP, saveMapFile, NodeConfig } from '../storage/nodeConfig';
8
+ import { generateSchemaWithAI, generateDungeonMapWithAI } from '../ai/claude';
9
+ import { completeTask, formatStats, formatAchievements, readProfile, addToUndoHistory, performUndo, getLastUndo } from '../storage/profile';
9
10
 
10
11
  // Parse tree ASCII art and create folder structure
11
12
  function createFoldersFromTree(rootPath: string, treeContent: string): void {
12
- // Create root folder
13
13
  if (!fs.existsSync(rootPath)) {
14
14
  fs.mkdirSync(rootPath, { recursive: true });
15
15
  }
16
16
 
17
- // Parse tree lines
18
17
  const lines = treeContent.split('\n');
19
18
  const stack: { path: string; indent: number }[] = [{ path: rootPath, indent: -1 }];
20
19
 
21
20
  for (const line of lines) {
22
- // Skip empty lines
23
21
  if (!line.trim()) continue;
24
22
 
25
- // Extract node name from tree line
26
- // Patterns: "├── Name", "└── Name", "│ ├── Name", etc.
27
23
  const match = line.match(/^([\s│]*)[├└]──\s*(.+)$/);
28
24
  if (!match) continue;
29
25
 
30
26
  const prefix = match[1];
31
27
  let nodeName = match[2].trim();
32
-
33
- // Calculate indent level (each │ or space block = 1 level)
34
28
  const indent = Math.floor(prefix.replace(/│/g, ' ').length / 4);
35
29
 
36
- // Clean node name - remove extra info in parentheses, brackets
30
+ // Extract metadata from node name
31
+ const isBoss = /\[BOSS\]/i.test(nodeName) || /\[MILESTONE\]/i.test(nodeName);
32
+ const deadlineMatch = nodeName.match(/\[(?:DUE|DEADLINE):\s*([^\]]+)\]/i);
33
+ const deadline = deadlineMatch ? deadlineMatch[1].trim() : undefined;
34
+
35
+ // Clean node name
37
36
  nodeName = nodeName.replace(/\s*\([^)]*\)\s*/g, '').trim();
38
37
  nodeName = nodeName.replace(/\s*\[[^\]]*\]\s*/g, '').trim();
39
38
 
40
- // Create safe folder name
41
39
  const safeName = nodeName
42
40
  .toLowerCase()
43
41
  .replace(/[^a-z0-9]+/g, '-')
@@ -45,7 +43,6 @@ function createFoldersFromTree(rootPath: string, treeContent: string): void {
45
43
 
46
44
  if (!safeName) continue;
47
45
 
48
- // Pop stack until we find parent
49
46
  while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
50
47
  stack.pop();
51
48
  }
@@ -53,25 +50,29 @@ function createFoldersFromTree(rootPath: string, treeContent: string): void {
53
50
  const parentPath = stack[stack.length - 1].path;
54
51
  const folderPath = path.join(parentPath, safeName);
55
52
 
56
- // Create folder
57
53
  if (!fs.existsSync(folderPath)) {
58
54
  fs.mkdirSync(folderPath, { recursive: true });
59
55
  }
60
56
 
61
- // Create node config
57
+ // Calculate depth for XP
58
+ const depth = stack.length;
59
+
62
60
  writeNodeConfig(folderPath, {
63
61
  name: nodeName,
62
+ status: 'open',
63
+ xp: calculateXP(depth, isBoss),
64
+ isBoss,
65
+ deadline,
64
66
  createdAt: new Date().toISOString(),
65
67
  updatedAt: new Date().toISOString(),
66
68
  });
67
69
 
68
- // Push to stack
69
70
  stack.push({ path: folderPath, indent });
70
71
  }
71
72
  }
72
73
 
73
74
  // Generate dungeon map visualization from folder structure
74
- function generateDungeonMap(dirPath: string): string {
75
+ function generateDungeonMap(dirPath: string, config: Config): string {
75
76
  if (!fs.existsSync(dirPath)) {
76
77
  return 'Directory does not exist.';
77
78
  }
@@ -86,32 +87,33 @@ function generateDungeonMap(dirPath: string): string {
86
87
  const lines: string[] = [];
87
88
  const roomWidth = 20;
88
89
  const roomsPerRow = 2;
89
- const wall = '';
90
+ const wall = '#';
90
91
  const door = '+';
91
92
  const task = '*';
92
93
  const milestone = '@';
94
+ const done = 'x';
95
+ const blocked = '!';
93
96
 
94
- // Group folders into rows of 2
95
97
  const rows: typeof folders[] = [];
96
98
  for (let i = 0; i < folders.length; i += roomsPerRow) {
97
99
  rows.push(folders.slice(i, i + roomsPerRow));
98
100
  }
99
101
 
100
- // Top border
102
+ lines.push('');
101
103
  lines.push(' ' + wall.repeat(roomWidth * roomsPerRow + 3));
102
104
 
103
105
  rows.forEach((row, rowIndex) => {
104
- // Room content
105
106
  for (let line = 0; line < 6; line++) {
106
107
  let rowStr = ' ' + wall;
107
108
 
108
109
  row.forEach((folder, colIndex) => {
109
- const name = folder.name.replace(/-/g, ' ');
110
+ const folderPath = path.join(dirPath, folder.name);
111
+ const nodeConfig = readNodeConfig(folderPath);
112
+ const name = (nodeConfig?.name || folder.name).replace(/-/g, ' ');
110
113
  const displayName = name.length > roomWidth - 4
111
114
  ? name.substring(0, roomWidth - 7) + '...'
112
115
  : name;
113
116
 
114
- // Get sub-items
115
117
  const subPath = path.join(dirPath, folder.name);
116
118
  const subEntries = fs.existsSync(subPath)
117
119
  ? fs.readdirSync(subPath, { withFileTypes: true })
@@ -119,25 +121,32 @@ function generateDungeonMap(dirPath: string): string {
119
121
  : [];
120
122
 
121
123
  if (line === 0) {
122
- // Empty line
123
124
  rowStr += ' '.repeat(roomWidth - 1) + wall;
124
125
  } else if (line === 1) {
125
- // Room name
126
+ const statusIcon = nodeConfig?.status === 'done' ? '[DONE]'
127
+ : nodeConfig?.isBoss ? '[BOSS]'
128
+ : '';
129
+ const title = statusIcon ? `${statusIcon}` : `[${displayName}]`;
130
+ const padding = roomWidth - title.length - 1;
131
+ rowStr += ' ' + title + ' '.repeat(Math.max(0, padding - 1)) + wall;
132
+ } else if (line === 2 && !nodeConfig?.isBoss) {
126
133
  const title = `[${displayName}]`;
127
134
  const padding = roomWidth - title.length - 1;
128
135
  rowStr += ' ' + title + ' '.repeat(Math.max(0, padding - 1)) + wall;
129
136
  } else if (line >= 2 && line <= 4) {
130
- // Sub-items
131
- const itemIndex = line - 2;
132
- if (itemIndex < subEntries.length) {
133
- const subName = subEntries[itemIndex].name.replace(/-/g, ' ');
137
+ const itemIndex = nodeConfig?.isBoss ? line - 2 : line - 3;
138
+ if (itemIndex >= 0 && itemIndex < subEntries.length) {
139
+ const subConfig = readNodeConfig(path.join(subPath, subEntries[itemIndex].name));
140
+ const subName = (subConfig?.name || subEntries[itemIndex].name).replace(/-/g, ' ');
134
141
  const shortName = subName.length > roomWidth - 6
135
142
  ? subName.substring(0, roomWidth - 9) + '...'
136
143
  : subName;
137
- const marker = subName.toLowerCase().includes('boss') ||
138
- subName.toLowerCase().includes('launch') ||
139
- subName.toLowerCase().includes('deploy')
140
- ? milestone : task;
144
+
145
+ let marker = task;
146
+ if (subConfig?.status === 'done') marker = done;
147
+ else if (subConfig?.status === 'blocked') marker = blocked;
148
+ else if (subConfig?.isBoss) marker = milestone;
149
+
141
150
  const itemStr = `${marker} ${shortName}`;
142
151
  const itemPadding = roomWidth - itemStr.length - 1;
143
152
  rowStr += ' ' + itemStr + ' '.repeat(Math.max(0, itemPadding - 1)) + wall;
@@ -145,11 +154,9 @@ function generateDungeonMap(dirPath: string): string {
145
154
  rowStr += ' '.repeat(roomWidth - 1) + wall;
146
155
  }
147
156
  } else {
148
- // Empty line
149
157
  rowStr += ' '.repeat(roomWidth - 1) + wall;
150
158
  }
151
159
 
152
- // Add door between rooms
153
160
  if (colIndex < row.length - 1 && line === 3) {
154
161
  rowStr = rowStr.slice(0, -1) + door + door + door;
155
162
  } else if (colIndex < row.length - 1) {
@@ -157,7 +164,6 @@ function generateDungeonMap(dirPath: string): string {
157
164
  }
158
165
  });
159
166
 
160
- // Fill empty space if odd number of rooms
161
167
  if (row.length < roomsPerRow) {
162
168
  rowStr += ' '.repeat(roomWidth) + wall;
163
169
  }
@@ -165,16 +171,14 @@ function generateDungeonMap(dirPath: string): string {
165
171
  lines.push(rowStr);
166
172
  }
167
173
 
168
- // Bottom border with doors to next row
169
174
  if (rowIndex < rows.length - 1) {
170
175
  let borderStr = ' ' + wall.repeat(Math.floor(roomWidth / 2)) + door;
171
176
  borderStr += wall.repeat(roomWidth - 1) + door;
172
177
  borderStr += wall.repeat(Math.floor(roomWidth / 2) + 1);
173
178
  lines.push(borderStr);
174
179
 
175
- // Corridor
176
- let corridorStr = ' ' + ' '.repeat(Math.floor(roomWidth / 2)) + '';
177
- corridorStr += ' '.repeat(roomWidth - 1) + '│';
180
+ let corridorStr = ' ' + ' '.repeat(Math.floor(roomWidth / 2)) + '|';
181
+ corridorStr += ' '.repeat(roomWidth - 1) + '|';
178
182
  lines.push(corridorStr);
179
183
 
180
184
  borderStr = ' ' + wall.repeat(Math.floor(roomWidth / 2)) + door;
@@ -184,23 +188,111 @@ function generateDungeonMap(dirPath: string): string {
184
188
  }
185
189
  });
186
190
 
187
- // Bottom border
188
191
  lines.push(' ' + wall.repeat(roomWidth * roomsPerRow + 3));
189
192
 
190
- // Legend
191
193
  lines.push('');
192
- lines.push(`Legend: ${task} Task ${milestone} Milestone ${door} Door ${wall} Wall`);
194
+ lines.push(`Legend: ${task} Task ${done} Done ${milestone} Boss/Milestone ${blocked} Blocked ${door} Door`);
193
195
 
194
196
  return lines.join('\n');
195
197
  }
196
198
 
199
+ // Parse human-readable date
200
+ function parseDeadline(input: string): string | null {
201
+ const lower = input.toLowerCase().trim();
202
+ const today = new Date();
203
+
204
+ if (lower === 'today') {
205
+ return today.toISOString().split('T')[0];
206
+ }
207
+
208
+ if (lower === 'tomorrow') {
209
+ today.setDate(today.getDate() + 1);
210
+ return today.toISOString().split('T')[0];
211
+ }
212
+
213
+ // +Nd format (e.g., +3d, +7d)
214
+ const plusDaysMatch = lower.match(/^\+(\d+)d$/);
215
+ if (plusDaysMatch) {
216
+ today.setDate(today.getDate() + parseInt(plusDaysMatch[1]));
217
+ return today.toISOString().split('T')[0];
218
+ }
219
+
220
+ // Try parsing as date
221
+ const parsed = new Date(input);
222
+ if (!isNaN(parsed.getTime())) {
223
+ return parsed.toISOString().split('T')[0];
224
+ }
225
+
226
+ return null;
227
+ }
228
+
229
+ // Format deadline for display
230
+ function formatDeadline(deadline: string): string {
231
+ const deadlineDate = new Date(deadline);
232
+ const today = new Date();
233
+ today.setHours(0, 0, 0, 0);
234
+ deadlineDate.setHours(0, 0, 0, 0);
235
+
236
+ const diffDays = Math.ceil((deadlineDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
237
+
238
+ if (diffDays < 0) return `OVERDUE ${Math.abs(diffDays)}d`;
239
+ if (diffDays === 0) return 'TODAY';
240
+ if (diffDays === 1) return 'tomorrow';
241
+ if (diffDays <= 7) return `${diffDays}d left`;
242
+
243
+ return deadlineDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
244
+ }
245
+
246
+ // Get depth of current path relative to storage root
247
+ function getDepth(currentPath: string, storagePath: string): number {
248
+ const relative = path.relative(storagePath, currentPath);
249
+ if (!relative) return 0;
250
+ return relative.split(path.sep).length;
251
+ }
252
+
253
+ // Mark node as done recursively
254
+ function markDoneRecursive(nodePath: string, storagePath: string): { xpGained: number; tasksCompleted: number; bossesDefeated: number } {
255
+ let result = { xpGained: 0, tasksCompleted: 0, bossesDefeated: 0 };
256
+
257
+ const config = readNodeConfig(nodePath);
258
+ if (!config || config.status === 'done') {
259
+ return result;
260
+ }
261
+
262
+ // First, mark all children as done
263
+ const entries = fs.readdirSync(nodePath, { withFileTypes: true });
264
+ for (const entry of entries) {
265
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
266
+ const childResult = markDoneRecursive(path.join(nodePath, entry.name), storagePath);
267
+ result.xpGained += childResult.xpGained;
268
+ result.tasksCompleted += childResult.tasksCompleted;
269
+ result.bossesDefeated += childResult.bossesDefeated;
270
+ }
271
+ }
272
+
273
+ // Mark this node as done
274
+ const depth = getDepth(nodePath, storagePath);
275
+ const xp = config.xp || calculateXP(depth, config.isBoss || false);
276
+
277
+ writeNodeConfig(nodePath, {
278
+ ...config,
279
+ status: 'done',
280
+ completedAt: new Date().toISOString(),
281
+ });
282
+
283
+ result.xpGained += xp;
284
+ result.tasksCompleted += 1;
285
+ if (config.isBoss) result.bossesDefeated += 1;
286
+
287
+ return result;
288
+ }
289
+
197
290
  export interface CommandResult {
198
291
  output?: string;
199
292
  newPath?: string;
200
293
  reloadConfig?: boolean;
201
294
  }
202
295
 
203
- // Pending schema waiting to be saved
204
296
  export interface PendingSchema {
205
297
  title: string;
206
298
  content: string;
@@ -208,25 +300,21 @@ export interface PendingSchema {
208
300
  tree?: any[];
209
301
  }
210
302
 
211
- // Conversation history for AI context
212
303
  export interface ConversationMessage {
213
304
  role: 'user' | 'assistant';
214
305
  content: string;
215
306
  }
216
307
 
217
- // Session state for dialog mode
218
308
  export interface SessionState {
219
309
  pending: PendingSchema | null;
220
310
  history: ConversationMessage[];
221
311
  }
222
312
 
223
- // Global session state
224
313
  export const sessionState: SessionState = {
225
314
  pending: null,
226
315
  history: []
227
316
  };
228
317
 
229
- // Format items in columns like native ls
230
318
  function formatColumns(items: string[], termWidth: number = 80): string {
231
319
  if (items.length === 0) return '';
232
320
 
@@ -242,7 +330,6 @@ function formatColumns(items: string[], termWidth: number = 80): string {
242
330
  return rows.join('\n');
243
331
  }
244
332
 
245
- // Copy to clipboard (cross-platform)
246
333
  function copyToClipboard(text: string): void {
247
334
  const platform = process.platform;
248
335
  try {
@@ -251,7 +338,6 @@ function copyToClipboard(text: string): void {
251
338
  } else if (platform === 'win32') {
252
339
  execSync('clip', { input: text });
253
340
  } else {
254
- // Linux - try xclip or xsel
255
341
  try {
256
342
  execSync('xclip -selection clipboard', { input: text });
257
343
  } catch {
@@ -259,11 +345,10 @@ function copyToClipboard(text: string): void {
259
345
  }
260
346
  }
261
347
  } catch (e) {
262
- // Silently fail if clipboard not available
348
+ // Silently fail
263
349
  }
264
350
  }
265
351
 
266
- // Helper function for recursive copy
267
352
  function copyRecursive(src: string, dest: string): void {
268
353
  const stat = fs.statSync(src);
269
354
 
@@ -283,6 +368,81 @@ function copyRecursive(src: string, dest: string): void {
283
368
  }
284
369
  }
285
370
 
371
+ // Build tree with status and deadline info
372
+ function getTreeWithStatus(
373
+ dirPath: string,
374
+ prefix: string = '',
375
+ isRoot: boolean = true,
376
+ maxDepth: number = 10,
377
+ currentDepth: number = 0,
378
+ showFiles: boolean = false
379
+ ): string[] {
380
+ const lines: string[] = [];
381
+
382
+ if (!fs.existsSync(dirPath)) {
383
+ return lines;
384
+ }
385
+
386
+ if (currentDepth >= maxDepth) {
387
+ return lines;
388
+ }
389
+
390
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
391
+ const dirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.'));
392
+ const files = showFiles ? entries.filter(e => e.isFile() && !e.name.startsWith('.')) : [];
393
+
394
+ const allItems = [...dirs, ...files];
395
+
396
+ allItems.forEach((entry, index) => {
397
+ const isLast = index === allItems.length - 1;
398
+ const connector = isLast ? '└── ' : '├── ';
399
+ const nextPrefix = isLast ? ' ' : '│ ';
400
+
401
+ if (entry.isDirectory()) {
402
+ const nodePath = path.join(dirPath, entry.name);
403
+ const config = readNodeConfig(nodePath);
404
+
405
+ let displayName = config?.name || entry.name;
406
+ const tags: string[] = [];
407
+
408
+ // Add status tag
409
+ if (config?.status === 'done') {
410
+ tags.push('DONE');
411
+ } else if (config?.status === 'blocked') {
412
+ tags.push('BLOCKED');
413
+ }
414
+
415
+ // Add boss tag
416
+ if (config?.isBoss) {
417
+ tags.push('BOSS');
418
+ }
419
+
420
+ // Add deadline tag
421
+ if (config?.deadline && config.status !== 'done') {
422
+ tags.push(formatDeadline(config.deadline));
423
+ }
424
+
425
+ const tagStr = tags.length > 0 ? ` [${tags.join('] [')}]` : '';
426
+
427
+ lines.push(`${prefix}${connector}${displayName}/${tagStr}`);
428
+
429
+ const childLines = getTreeWithStatus(
430
+ nodePath,
431
+ prefix + nextPrefix,
432
+ false,
433
+ maxDepth,
434
+ currentDepth + 1,
435
+ showFiles
436
+ );
437
+ lines.push(...childLines);
438
+ } else {
439
+ lines.push(`${prefix}${connector}${entry.name}`);
440
+ }
441
+ });
442
+
443
+ return lines;
444
+ }
445
+
286
446
  export async function processCommand(
287
447
  input: string,
288
448
  currentPath: string,
@@ -290,7 +450,6 @@ export async function processCommand(
290
450
  signal?: AbortSignal,
291
451
  rl?: readline.Interface
292
452
  ): Promise<CommandResult> {
293
- // Check for clipboard pipe
294
453
  const clipboardPipe = /\s*\|\s*(pbcopy|copy|clip)\s*$/i;
295
454
  const shouldCopy = clipboardPipe.test(input);
296
455
  const cleanInput = input.replace(clipboardPipe, '').trim();
@@ -298,7 +457,6 @@ export async function processCommand(
298
457
  const parts = cleanInput.split(' ').filter(p => p.length > 0);
299
458
  const command = parts[0].toLowerCase();
300
459
 
301
- // Helper to wrap result with clipboard copy
302
460
  const wrapResult = (result: CommandResult): CommandResult => {
303
461
  if (shouldCopy && result.output) {
304
462
  copyToClipboard(result.output);
@@ -313,9 +471,350 @@ export async function processCommand(
313
471
  return wrapResult({ output: `Roguelike CLI v${pkg.version}` });
314
472
  }
315
473
 
474
+ // Stats command
475
+ if (command === 'stats') {
476
+ return wrapResult({ output: formatStats() });
477
+ }
478
+
479
+ // Achievements command
480
+ if (command === 'achievements' || command === 'ach') {
481
+ return wrapResult({ output: formatAchievements() });
482
+ }
483
+
484
+ // Done command - mark current node as completed
485
+ if (command === 'done') {
486
+ const nodeConfig = readNodeConfig(currentPath);
487
+
488
+ if (!nodeConfig) {
489
+ return wrapResult({ output: 'No task at current location. Navigate to a task first.' });
490
+ }
491
+
492
+ if (nodeConfig.status === 'done') {
493
+ return wrapResult({ output: 'This task is already completed.' });
494
+ }
495
+
496
+ // Mark done recursively
497
+ const result = markDoneRecursive(currentPath, config.storagePath);
498
+
499
+ // Update profile with XP and achievements
500
+ const depth = getDepth(currentPath, config.storagePath);
501
+ const taskResult = completeTask(
502
+ result.xpGained,
503
+ nodeConfig.isBoss || false,
504
+ depth,
505
+ nodeConfig.createdAt
506
+ );
507
+
508
+ // 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
+
516
+ let output = `\n=== TASK COMPLETED ===\n`;
517
+ output += `\nTasks completed: ${result.tasksCompleted}`;
518
+ if (result.bossesDefeated > 0) {
519
+ output += `\nBosses defeated: ${result.bossesDefeated}`;
520
+ }
521
+ output += `\n+${result.xpGained} XP`;
522
+
523
+ if (taskResult.levelUp) {
524
+ output += `\n\n*** LEVEL UP! ***`;
525
+ output += `\nYou are now level ${taskResult.newLevel}!`;
526
+ }
527
+
528
+ if (taskResult.newAchievements.length > 0) {
529
+ output += `\n\n=== NEW ACHIEVEMENTS ===`;
530
+ for (const ach of taskResult.newAchievements) {
531
+ output += `\n[x] ${ach.name}: ${ach.description}`;
532
+ }
533
+ }
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}`;
564
+ output += '\n';
565
+
566
+ return wrapResult({ output });
567
+ }
568
+
569
+ // Deadline command
570
+ if (command === 'deadline') {
571
+ if (parts.length < 2) {
572
+ return wrapResult({ output: 'Usage: deadline <date>\nExamples: deadline today, deadline tomorrow, deadline +3d, deadline Jan 15' });
573
+ }
574
+
575
+ const dateStr = parts.slice(1).join(' ');
576
+ const deadline = parseDeadline(dateStr);
577
+
578
+ if (!deadline) {
579
+ return wrapResult({ output: `Could not parse date: ${dateStr}` });
580
+ }
581
+
582
+ const nodeConfig = readNodeConfig(currentPath);
583
+ if (!nodeConfig) {
584
+ return wrapResult({ output: 'No task at current location.' });
585
+ }
586
+
587
+ writeNodeConfig(currentPath, {
588
+ ...nodeConfig,
589
+ deadline,
590
+ });
591
+
592
+ return wrapResult({ output: `Deadline set: ${formatDeadline(deadline)}` });
593
+ }
594
+
595
+ // Boss command - mark as boss/milestone
596
+ if (command === 'boss' || command === 'milestone') {
597
+ const nodeConfig = readNodeConfig(currentPath);
598
+ if (!nodeConfig) {
599
+ return wrapResult({ output: 'No task at current location.' });
600
+ }
601
+
602
+ const newIsBoss = !nodeConfig.isBoss;
603
+ const depth = getDepth(currentPath, config.storagePath);
604
+
605
+ writeNodeConfig(currentPath, {
606
+ ...nodeConfig,
607
+ isBoss: newIsBoss,
608
+ xp: calculateXP(depth, newIsBoss),
609
+ });
610
+
611
+ return wrapResult({ output: newIsBoss ? 'Marked as BOSS task (3x XP)' : 'Removed BOSS status' });
612
+ }
613
+
614
+ // Block command - block <node> or block "reason"
615
+ if (command === 'block') {
616
+ const nodeConfig = readNodeConfig(currentPath);
617
+ if (!nodeConfig) {
618
+ return wrapResult({ output: 'No task at current location.' });
619
+ }
620
+
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
+ }
651
+
652
+ writeNodeConfig(currentPath, {
653
+ ...nodeConfig,
654
+ status: 'blocked',
655
+ blockedBy,
656
+ });
657
+
658
+ return wrapResult({ output: blockMessage });
659
+ }
660
+
661
+ // Unblock command
662
+ if (command === 'unblock') {
663
+ const nodeConfig = readNodeConfig(currentPath);
664
+ if (!nodeConfig) {
665
+ return wrapResult({ output: 'No task at current location.' });
666
+ }
667
+
668
+ writeNodeConfig(currentPath, {
669
+ ...nodeConfig,
670
+ status: 'open',
671
+ blockedBy: [],
672
+ });
673
+
674
+ return wrapResult({ output: 'Task unblocked' });
675
+ }
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
+
751
+ // Status command - show current task status
752
+ if (command === 'status') {
753
+ const nodeConfig = readNodeConfig(currentPath);
754
+ if (!nodeConfig) {
755
+ return wrapResult({ output: 'No task at current location.' });
756
+ }
757
+
758
+ const lines: string[] = [
759
+ '',
760
+ `Task: ${nodeConfig.name}`,
761
+ `Status: ${nodeConfig.status.toUpperCase()}`,
762
+ `XP: ${nodeConfig.xp}`,
763
+ ];
764
+
765
+ if (nodeConfig.isBoss) {
766
+ lines.push('Type: BOSS');
767
+ }
768
+
769
+ if (nodeConfig.deadline) {
770
+ lines.push(`Deadline: ${formatDeadline(nodeConfig.deadline)}`);
771
+ }
772
+
773
+ if (nodeConfig.completedAt) {
774
+ lines.push(`Completed: ${new Date(nodeConfig.completedAt).toLocaleDateString()}`);
775
+ }
776
+
777
+ if (nodeConfig.blockedBy && nodeConfig.blockedBy.length > 0) {
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(', ')}`);
787
+ }
788
+
789
+ lines.push('');
790
+
791
+ return wrapResult({ output: lines.join('\n') });
792
+ }
793
+
316
794
  // Map command - dungeon visualization
317
795
  if (command === 'map') {
318
- const dungeonMap = generateDungeonMap(currentPath);
796
+ // Check for --ai flag to use AI generation
797
+ if (parts.includes('--ai') || parts.includes('-a')) {
798
+ const treeLines = getTreeWithStatus(currentPath, '', true, 10, 0, false);
799
+ const treeContent = treeLines.join('\n');
800
+
801
+ if (!treeContent) {
802
+ return wrapResult({ output: 'No tasks to visualize.' });
803
+ }
804
+
805
+ const mapContent = await generateDungeonMapWithAI(treeContent, config, signal);
806
+
807
+ if (mapContent) {
808
+ // Save map to file
809
+ const folderName = path.basename(currentPath);
810
+ saveMapFile(currentPath, folderName + '-map', mapContent);
811
+ return wrapResult({ output: mapContent + '\n\n[Map saved as .rlc.map]' });
812
+ }
813
+
814
+ return wrapResult({ output: 'Could not generate AI map. Using default.' });
815
+ }
816
+
817
+ const dungeonMap = generateDungeonMap(currentPath, config);
319
818
  return wrapResult({ output: dungeonMap });
320
819
  }
321
820
 
@@ -331,7 +830,15 @@ export async function processCommand(
331
830
  if (entry.name.startsWith('.')) continue;
332
831
 
333
832
  if (entry.isDirectory()) {
334
- items.push(entry.name + '/');
833
+ const nodePath = path.join(currentPath, entry.name);
834
+ const config = readNodeConfig(nodePath);
835
+ let suffix = '/';
836
+
837
+ if (config?.status === 'done') suffix = '/ [DONE]';
838
+ else if (config?.status === 'blocked') suffix = '/ [BLOCKED]';
839
+ else if (config?.isBoss) suffix = '/ [BOSS]';
840
+
841
+ items.push(entry.name + suffix);
335
842
  } else {
336
843
  items.push(entry.name);
337
844
  }
@@ -348,7 +855,6 @@ export async function processCommand(
348
855
  if (command === 'tree') {
349
856
  const showFiles = parts.includes('-A') || parts.includes('--all');
350
857
 
351
- // Parse depth: --depth=N or -d N
352
858
  let maxDepth = 10;
353
859
  const depthFlag = parts.find(p => p.startsWith('--depth='));
354
860
  if (depthFlag) {
@@ -360,20 +866,18 @@ export async function processCommand(
360
866
  }
361
867
  }
362
868
 
363
- const treeLines = getTree(currentPath, '', true, maxDepth, 0, showFiles);
869
+ const treeLines = getTreeWithStatus(currentPath, '', true, maxDepth, 0, showFiles);
364
870
  if (treeLines.length === 0) {
365
871
  return wrapResult({ output: 'No items found.' });
366
872
  }
367
873
  return wrapResult({ output: treeLines.join('\n') });
368
874
  }
369
875
 
370
- // Handle navigation without 'cd' command (.., ..., ...., etc)
876
+ // Handle navigation without 'cd' command
371
877
  if (/^\.{2,}$/.test(command)) {
372
- // Count dots: .. = 1 level, ... = 2 levels, .... = 3 levels, etc
373
878
  const levels = command.length - 1;
374
879
  let targetPath = currentPath;
375
880
 
376
- // Already at root?
377
881
  if (targetPath === config.storagePath) {
378
882
  return { output: 'Already at root.' };
379
883
  }
@@ -381,14 +885,12 @@ export async function processCommand(
381
885
  for (let i = 0; i < levels; i++) {
382
886
  const parentPath = path.dirname(targetPath);
383
887
 
384
- // Stop at storage root
385
888
  if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
386
889
  break;
387
890
  }
388
891
 
389
892
  targetPath = parentPath;
390
893
 
391
- // If we reached root, stop
392
894
  if (targetPath === config.storagePath) {
393
895
  break;
394
896
  }
@@ -455,7 +957,6 @@ export async function processCommand(
455
957
  fs.renameSync(sourcePath, destPath);
456
958
  return { output: `Moved: ${source} -> ${dest}` };
457
959
  } catch (error: any) {
458
- // If rename fails (cross-device), copy then delete
459
960
  try {
460
961
  copyRecursive(sourcePath, destPath);
461
962
  fs.rmSync(sourcePath, { recursive: true, force: true });
@@ -469,7 +970,6 @@ export async function processCommand(
469
970
  if (command === 'open') {
470
971
  const { exec } = require('child_process');
471
972
 
472
- // open or open . - open current folder in system file manager
473
973
  if (parts.length < 2 || parts[1] === '.') {
474
974
  exec(`open "${currentPath}"`);
475
975
  return { output: `Opening: ${currentPath}` };
@@ -478,18 +978,15 @@ export async function processCommand(
478
978
  const name = parts.slice(1).join(' ');
479
979
  const targetPath = path.join(currentPath, name);
480
980
 
481
- // Check if target exists
482
981
  if (fs.existsSync(targetPath)) {
483
982
  const stat = fs.statSync(targetPath);
484
983
 
485
984
  if (stat.isDirectory()) {
486
- // It's a folder, open in file manager
487
985
  exec(`open "${targetPath}"`);
488
986
  return { output: `Opening: ${targetPath}` };
489
987
  }
490
988
 
491
989
  if (stat.isFile()) {
492
- // It's a file, show its content (supports | pbcopy)
493
990
  const content = fs.readFileSync(targetPath, 'utf-8');
494
991
  return wrapResult({ output: content });
495
992
  }
@@ -545,12 +1042,10 @@ export async function processCommand(
545
1042
 
546
1043
  const target = parts.slice(1).join(' ');
547
1044
 
548
- // Handle cd .., cd ..., cd ...., etc
549
1045
  if (/^\.{2,}$/.test(target)) {
550
1046
  const levels = target.length - 1;
551
1047
  let targetPath = currentPath;
552
1048
 
553
- // Already at root?
554
1049
  if (targetPath === config.storagePath) {
555
1050
  return { output: 'Already at root.' };
556
1051
  }
@@ -558,14 +1053,12 @@ export async function processCommand(
558
1053
  for (let i = 0; i < levels; i++) {
559
1054
  const parentPath = path.dirname(targetPath);
560
1055
 
561
- // Stop at storage root
562
1056
  if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
563
1057
  break;
564
1058
  }
565
1059
 
566
1060
  targetPath = parentPath;
567
1061
 
568
- // If we reached root, stop
569
1062
  if (targetPath === config.storagePath) {
570
1063
  break;
571
1064
  }
@@ -574,14 +1067,12 @@ export async function processCommand(
574
1067
  return { newPath: targetPath, output: '' };
575
1068
  }
576
1069
 
577
- // Handle paths like "cd bank/account" or "cd ../other"
578
1070
  if (target.includes('/')) {
579
1071
  let targetPath = currentPath;
580
1072
  const pathParts = target.split('/');
581
1073
 
582
1074
  for (const part of pathParts) {
583
1075
  if (/^\.{2,}$/.test(part)) {
584
- // Handle .., ..., ...., etc in path
585
1076
  const levels = part.length - 1;
586
1077
  for (let i = 0; i < levels; i++) {
587
1078
  if (targetPath === config.storagePath) break;
@@ -656,121 +1147,71 @@ Storage: ${config.storagePath}
656
1147
 
657
1148
  if (command === 'help') {
658
1149
  return wrapResult({
659
- output: `Commands:
660
- init - Initialize rlc (first time setup)
661
- ls - List all schemas, todos, and notes
662
- tree - Show directory tree structure
663
- tree -A - Show tree with files
664
- tree --depth=N - Limit tree depth (e.g., --depth=2)
665
- map - Dungeon map visualization
666
- cd <node> - Navigate into a node
667
- cd .. - Go back to parent
668
- pwd - Show current path
669
- open - Open current folder in Finder
670
- open <folder> - Open specific folder in Finder
671
- mkdir <name> - Create new folder
672
- cp <src> <dest> - Copy file or folder
673
- mv <src> <dest> - Move/rename file or folder
674
- rm <name> - Delete file
675
- rm -rf <name> - Delete folder recursively
676
- config - Show configuration
677
- config:apiKey=<key> - Set API key
678
- v, version - Show version
679
- <description> - Create schema/todo (AI generates preview)
680
- save - Save pending schema to disk
681
- cancel - Discard pending schema
682
- clean - Show items to delete in current folder
683
- clean --yes - Delete all items in current folder
684
- exit/quit - Exit the program
685
-
686
- Clipboard:
687
- ls | pbcopy - Copy output to clipboard (macOS)
688
- tree | pbcopy - Works with any command
689
- config | copy - Alternative for Windows
690
-
691
- Workflow:
692
- 1. Type description (e.g., "todo: deploy app")
693
- 2. AI generates schema preview
694
- 3. Refine with more instructions if needed
695
- 4. Type "save" to save or "cancel" to discard
696
-
697
- Examples:
1150
+ output: `
1151
+ === ROGUELIKE CLI ===
698
1152
 
699
- > todo opening company in delaware
1153
+ Navigation:
1154
+ ls List tasks and files
1155
+ tree Show task tree with status
1156
+ tree -A Include files
1157
+ tree --depth=N Limit tree depth
1158
+ cd <task> Navigate into task
1159
+ cd .., ... Go back 1 or 2 levels
1160
+ pwd Show current path
1161
+ open Open folder in Finder
700
1162
 
701
- ┌─ TODO opening company in delaware ───────────────────────────┐
702
- │ │
703
- ├── register business name │
704
- ├── file incorporation papers │
705
- ├── get EIN number │
706
- └── Branch: legal │
707
- └── open business bank account │
708
- │ │
709
- └───────────────────────────────────────────────────────────────┘
1163
+ Task Management:
1164
+ mkdir <name> Create new task
1165
+ done Mark current task as completed (recursive)
1166
+ undo Undo last done (restores XP)
1167
+ deadline <date> Set deadline (today, tomorrow, +3d, Jan 15)
1168
+ boss Toggle boss/milestone status (3x XP)
1169
+ block [node] Block by task (or text reason)
1170
+ unblock Remove blocked status
1171
+ status Show current task details
1172
+ check Show overdue/upcoming deadlines
710
1173
 
711
- > yandex cloud production infrastructure
1174
+ File Operations:
1175
+ cp <src> <dest> Copy file or folder
1176
+ mv <src> <dest> Move/rename
1177
+ rm <name> Delete file
1178
+ rm -rf <name> Delete folder
712
1179
 
713
- ┌─────────────────────────────────────────────────────────────┐
714
- │ Yandex Cloud │
715
- │ │
716
- │ ┌──────────────────┐ ┌──────────────────┐ │
717
- │ │ back-fastapi │ │ admin-next │ │
718
- │ │ (VM) │ │ (VM) │ │
719
- │ └────────┬─────────┘ └──────────────────┘ │
720
- │ │ │
721
- │ ├──────────────────┬─────────────────┐ │
722
- │ │ │ │ │
723
- │ ┌────────▼────────┐ ┌─────▼──────┐ ┌──────▼────────┐ │
724
- │ │ PostgreSQL │ │ Redis │ │ Cloudflare │ │
725
- │ │ (Existing DB) │ │ Cluster │ │ R2 Storage │ │
726
- │ └─────────────────┘ └────────────┘ └───────────────┘ │
727
- └─────────────────────────────────────────────────────────────┘
1180
+ Gamification:
1181
+ stats Show XP, level, streaks
1182
+ achievements Show achievement list
1183
+ map Dungeon map view
1184
+ map --ai AI-generated dungeon map
728
1185
 
729
- > architecture production redis web application
1186
+ Schema Generation:
1187
+ <description> AI generates todo/schema preview
1188
+ save Save pending schema
1189
+ cancel Discard pending schema
730
1190
 
731
- ┌─ Architecture production redis web application ────────────┐
732
- │ │
733
- ├── load-balancer │
734
- ├── web-servers │
735
- │ ├── app-server-1 │
736
- │ ├── app-server-2 │
737
- │ └── app-server-3 │
738
- ├── redis │
739
- │ ├── cache-cluster │
740
- │ └── session-store │
741
- └── database │
742
- ├── postgres-primary │
743
- └── postgres-replica │
744
- │ │
745
- └───────────────────────────────────────────────────────────────┘
1191
+ Utility:
1192
+ init Setup wizard
1193
+ config Show settings
1194
+ clean --yes Clear current folder
1195
+ v, version Show version
1196
+ help This help
1197
+ exit, quit Exit
746
1198
 
747
- > kubernetes cluster with clusters postgres and redis
1199
+ Clipboard:
1200
+ <cmd> | pbcopy Copy output (macOS)
1201
+ <cmd> | clip Copy output (Windows)
748
1202
 
749
- ┌─────────────────────────────────────────────────────────────┐
750
- │ Kubernetes cluster with clusters postgres │
751
- │ │
752
- │ ┌──────────────┐ ┌──────────────┐ │
753
- │ │ postgres │ │ redis │ │
754
- │ │ │ │ │ │
755
- │ │ primary-pod │ │ cache-pod-1 │ │
756
- │ │ replica-pod-1│ │ cache-pod-2 │ │
757
- │ │ replica-pod-2│ │ │ │
758
- │ └──────┬───────┘ └──────┬───────┘ │
759
- │ │ │ │
760
- │ └──────────┬───────────┘ │
761
- │ │ │
762
- │ ┌───────▼────────┐ │
763
- │ │ worker-zones │ │
764
- │ │ zone-1 │ │
765
- │ │ zone-2 │ │
766
- │ └────────────────┘ │
767
- └─────────────────────────────────────────────────────────────┘
1203
+ Examples:
1204
+ block backend-api Block current task by sibling task
1205
+ block "waiting for design" Block with text reason
1206
+ deadline +3d Due in 3 days
1207
+ check See all upcoming deadlines
768
1208
 
769
- www.rlc.rocks`
1209
+ www.rlc.rocks
1210
+ `.trim()
770
1211
  });
771
1212
  }
772
1213
 
773
- // Save command - save pending schema/todo
1214
+ // Save command
774
1215
  if (command === 'save') {
775
1216
  if (!sessionState.pending) {
776
1217
  return wrapResult({ output: 'Nothing to save. Create a schema first.' });
@@ -780,17 +1221,14 @@ www.rlc.rocks`
780
1221
  const safeName = pending.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
781
1222
 
782
1223
  if (pending.format === 'tree') {
783
- // Create folder structure from tree ASCII art
784
1224
  const rootPath = path.join(currentPath, safeName);
785
1225
  createFoldersFromTree(rootPath, pending.content);
786
1226
 
787
- // Clear session
788
1227
  sessionState.pending = null;
789
1228
  sessionState.history = [];
790
1229
 
791
1230
  return wrapResult({ output: `Created todo folder: ${safeName}/` });
792
1231
  } else {
793
- // Save as .rlc.schema file
794
1232
  const schemaPath = saveSchemaFile(
795
1233
  currentPath,
796
1234
  pending.title,
@@ -798,7 +1236,6 @@ www.rlc.rocks`
798
1236
  );
799
1237
  const filename = path.basename(schemaPath);
800
1238
 
801
- // Clear session
802
1239
  sessionState.pending = null;
803
1240
  sessionState.history = [];
804
1241
 
@@ -806,7 +1243,7 @@ www.rlc.rocks`
806
1243
  }
807
1244
  }
808
1245
 
809
- // Cancel command - discard pending schema
1246
+ // Cancel command
810
1247
  if (command === 'cancel') {
811
1248
  if (!sessionState.pending) {
812
1249
  return wrapResult({ output: 'Nothing to cancel.' });
@@ -818,7 +1255,7 @@ www.rlc.rocks`
818
1255
  return wrapResult({ output: 'Discarded pending schema.' });
819
1256
  }
820
1257
 
821
- // Clean command - clear current directory
1258
+ // Clean command
822
1259
  if (command === 'clean') {
823
1260
  const entries = fs.readdirSync(currentPath);
824
1261
  const toDelete = entries.filter(e => !e.startsWith('.'));
@@ -827,7 +1264,6 @@ www.rlc.rocks`
827
1264
  return wrapResult({ output: 'Directory is already empty.' });
828
1265
  }
829
1266
 
830
- // Check for --yes flag to skip confirmation
831
1267
  if (!parts.includes('--yes') && !parts.includes('-y')) {
832
1268
  return wrapResult({
833
1269
  output: `Will delete ${toDelete.length} items:\n${toDelete.join('\n')}\n\nRun "clean --yes" to confirm.`
@@ -842,10 +1278,9 @@ www.rlc.rocks`
842
1278
  return wrapResult({ output: `Deleted ${toDelete.length} items.` });
843
1279
  }
844
1280
 
845
- // AI generation - store in pending, don't save immediately
1281
+ // AI generation
846
1282
  const fullInput = cleanInput;
847
1283
 
848
- // Add user message to history
849
1284
  sessionState.history.push({ role: 'user', content: fullInput });
850
1285
 
851
1286
  const schema = await generateSchemaWithAI(fullInput, config, signal, sessionState.history);
@@ -855,7 +1290,6 @@ www.rlc.rocks`
855
1290
  }
856
1291
 
857
1292
  if (schema) {
858
- // Store in pending
859
1293
  sessionState.pending = {
860
1294
  title: schema.title,
861
1295
  content: schema.content,
@@ -863,7 +1297,6 @@ www.rlc.rocks`
863
1297
  tree: schema.tree
864
1298
  };
865
1299
 
866
- // Add assistant response to history
867
1300
  sessionState.history.push({ role: 'assistant', content: schema.content });
868
1301
 
869
1302
  const safeName = schema.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');