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.
- package/README.md +154 -208
- package/dist/ai/claude.js +53 -42
- package/dist/interactive/commands.js +417 -189
- package/dist/interactive/startup.js +6 -3
- package/dist/storage/nodeConfig.js +35 -1
- package/dist/storage/profile.js +276 -0
- package/package.json +1 -1
- package/src/ai/claude.ts +60 -43
- package/src/interactive/commands.ts +512 -196
- package/src/interactive/startup.ts +6 -3
- package/src/storage/nodeConfig.ts +48 -2
- package/src/storage/profile.ts +281 -0
|
@@ -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 } 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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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 ${
|
|
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
|
|
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,202 @@ 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
|
+
let output = `\n=== TASK COMPLETED ===\n`;
|
|
509
|
+
output += `\nTasks completed: ${result.tasksCompleted}`;
|
|
510
|
+
if (result.bossesDefeated > 0) {
|
|
511
|
+
output += `\nBosses defeated: ${result.bossesDefeated}`;
|
|
512
|
+
}
|
|
513
|
+
output += `\n+${result.xpGained} XP`;
|
|
514
|
+
|
|
515
|
+
if (taskResult.levelUp) {
|
|
516
|
+
output += `\n\n*** LEVEL UP! ***`;
|
|
517
|
+
output += `\nYou are now level ${taskResult.newLevel}!`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (taskResult.newAchievements.length > 0) {
|
|
521
|
+
output += `\n\n=== NEW ACHIEVEMENTS ===`;
|
|
522
|
+
for (const ach of taskResult.newAchievements) {
|
|
523
|
+
output += `\n[x] ${ach.name}: ${ach.description}`;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
output += '\n';
|
|
528
|
+
|
|
529
|
+
return wrapResult({ output });
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Deadline command
|
|
533
|
+
if (command === 'deadline') {
|
|
534
|
+
if (parts.length < 2) {
|
|
535
|
+
return wrapResult({ output: 'Usage: deadline <date>\nExamples: deadline today, deadline tomorrow, deadline +3d, deadline Jan 15' });
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const dateStr = parts.slice(1).join(' ');
|
|
539
|
+
const deadline = parseDeadline(dateStr);
|
|
540
|
+
|
|
541
|
+
if (!deadline) {
|
|
542
|
+
return wrapResult({ output: `Could not parse date: ${dateStr}` });
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const nodeConfig = readNodeConfig(currentPath);
|
|
546
|
+
if (!nodeConfig) {
|
|
547
|
+
return wrapResult({ output: 'No task at current location.' });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
writeNodeConfig(currentPath, {
|
|
551
|
+
...nodeConfig,
|
|
552
|
+
deadline,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
return wrapResult({ output: `Deadline set: ${formatDeadline(deadline)}` });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Boss command - mark as boss/milestone
|
|
559
|
+
if (command === 'boss' || command === 'milestone') {
|
|
560
|
+
const nodeConfig = readNodeConfig(currentPath);
|
|
561
|
+
if (!nodeConfig) {
|
|
562
|
+
return wrapResult({ output: 'No task at current location.' });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const newIsBoss = !nodeConfig.isBoss;
|
|
566
|
+
const depth = getDepth(currentPath, config.storagePath);
|
|
567
|
+
|
|
568
|
+
writeNodeConfig(currentPath, {
|
|
569
|
+
...nodeConfig,
|
|
570
|
+
isBoss: newIsBoss,
|
|
571
|
+
xp: calculateXP(depth, newIsBoss),
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
return wrapResult({ output: newIsBoss ? 'Marked as BOSS task (3x XP)' : 'Removed BOSS status' });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Block command
|
|
578
|
+
if (command === 'block') {
|
|
579
|
+
const nodeConfig = readNodeConfig(currentPath);
|
|
580
|
+
if (!nodeConfig) {
|
|
581
|
+
return wrapResult({ output: 'No task at current location.' });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const reason = parts.length > 1 ? parts.slice(1).join(' ') : undefined;
|
|
585
|
+
|
|
586
|
+
writeNodeConfig(currentPath, {
|
|
587
|
+
...nodeConfig,
|
|
588
|
+
status: 'blocked',
|
|
589
|
+
blockedBy: reason ? [reason] : nodeConfig.blockedBy,
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
return wrapResult({ output: reason ? `Blocked: ${reason}` : 'Task marked as blocked' });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Unblock command
|
|
596
|
+
if (command === 'unblock') {
|
|
597
|
+
const nodeConfig = readNodeConfig(currentPath);
|
|
598
|
+
if (!nodeConfig) {
|
|
599
|
+
return wrapResult({ output: 'No task at current location.' });
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
writeNodeConfig(currentPath, {
|
|
603
|
+
...nodeConfig,
|
|
604
|
+
status: 'open',
|
|
605
|
+
blockedBy: [],
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
return wrapResult({ output: 'Task unblocked' });
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Status command - show current task status
|
|
612
|
+
if (command === 'status') {
|
|
613
|
+
const nodeConfig = readNodeConfig(currentPath);
|
|
614
|
+
if (!nodeConfig) {
|
|
615
|
+
return wrapResult({ output: 'No task at current location.' });
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const lines: string[] = [
|
|
619
|
+
'',
|
|
620
|
+
`Task: ${nodeConfig.name}`,
|
|
621
|
+
`Status: ${nodeConfig.status.toUpperCase()}`,
|
|
622
|
+
`XP: ${nodeConfig.xp}`,
|
|
623
|
+
];
|
|
624
|
+
|
|
625
|
+
if (nodeConfig.isBoss) {
|
|
626
|
+
lines.push('Type: BOSS');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (nodeConfig.deadline) {
|
|
630
|
+
lines.push(`Deadline: ${formatDeadline(nodeConfig.deadline)}`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (nodeConfig.completedAt) {
|
|
634
|
+
lines.push(`Completed: ${new Date(nodeConfig.completedAt).toLocaleDateString()}`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (nodeConfig.blockedBy && nodeConfig.blockedBy.length > 0) {
|
|
638
|
+
lines.push(`Blocked by: ${nodeConfig.blockedBy.join(', ')}`);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
lines.push('');
|
|
642
|
+
|
|
643
|
+
return wrapResult({ output: lines.join('\n') });
|
|
644
|
+
}
|
|
645
|
+
|
|
316
646
|
// Map command - dungeon visualization
|
|
317
647
|
if (command === 'map') {
|
|
318
|
-
|
|
648
|
+
// Check for --ai flag to use AI generation
|
|
649
|
+
if (parts.includes('--ai') || parts.includes('-a')) {
|
|
650
|
+
const treeLines = getTreeWithStatus(currentPath, '', true, 10, 0, false);
|
|
651
|
+
const treeContent = treeLines.join('\n');
|
|
652
|
+
|
|
653
|
+
if (!treeContent) {
|
|
654
|
+
return wrapResult({ output: 'No tasks to visualize.' });
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const mapContent = await generateDungeonMapWithAI(treeContent, config, signal);
|
|
658
|
+
|
|
659
|
+
if (mapContent) {
|
|
660
|
+
// Save map to file
|
|
661
|
+
const folderName = path.basename(currentPath);
|
|
662
|
+
saveMapFile(currentPath, folderName + '-map', mapContent);
|
|
663
|
+
return wrapResult({ output: mapContent + '\n\n[Map saved as .rlc.map]' });
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return wrapResult({ output: 'Could not generate AI map. Using default.' });
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const dungeonMap = generateDungeonMap(currentPath, config);
|
|
319
670
|
return wrapResult({ output: dungeonMap });
|
|
320
671
|
}
|
|
321
672
|
|
|
@@ -331,7 +682,15 @@ export async function processCommand(
|
|
|
331
682
|
if (entry.name.startsWith('.')) continue;
|
|
332
683
|
|
|
333
684
|
if (entry.isDirectory()) {
|
|
334
|
-
|
|
685
|
+
const nodePath = path.join(currentPath, entry.name);
|
|
686
|
+
const config = readNodeConfig(nodePath);
|
|
687
|
+
let suffix = '/';
|
|
688
|
+
|
|
689
|
+
if (config?.status === 'done') suffix = '/ [DONE]';
|
|
690
|
+
else if (config?.status === 'blocked') suffix = '/ [BLOCKED]';
|
|
691
|
+
else if (config?.isBoss) suffix = '/ [BOSS]';
|
|
692
|
+
|
|
693
|
+
items.push(entry.name + suffix);
|
|
335
694
|
} else {
|
|
336
695
|
items.push(entry.name);
|
|
337
696
|
}
|
|
@@ -348,7 +707,6 @@ export async function processCommand(
|
|
|
348
707
|
if (command === 'tree') {
|
|
349
708
|
const showFiles = parts.includes('-A') || parts.includes('--all');
|
|
350
709
|
|
|
351
|
-
// Parse depth: --depth=N or -d N
|
|
352
710
|
let maxDepth = 10;
|
|
353
711
|
const depthFlag = parts.find(p => p.startsWith('--depth='));
|
|
354
712
|
if (depthFlag) {
|
|
@@ -360,24 +718,34 @@ export async function processCommand(
|
|
|
360
718
|
}
|
|
361
719
|
}
|
|
362
720
|
|
|
363
|
-
const treeLines =
|
|
721
|
+
const treeLines = getTreeWithStatus(currentPath, '', true, maxDepth, 0, showFiles);
|
|
364
722
|
if (treeLines.length === 0) {
|
|
365
723
|
return wrapResult({ output: 'No items found.' });
|
|
366
724
|
}
|
|
367
725
|
return wrapResult({ output: treeLines.join('\n') });
|
|
368
726
|
}
|
|
369
727
|
|
|
370
|
-
// Handle navigation without 'cd' command
|
|
371
|
-
if (command
|
|
372
|
-
|
|
728
|
+
// Handle navigation without 'cd' command
|
|
729
|
+
if (/^\.{2,}$/.test(command)) {
|
|
730
|
+
const levels = command.length - 1;
|
|
373
731
|
let targetPath = currentPath;
|
|
374
732
|
|
|
733
|
+
if (targetPath === config.storagePath) {
|
|
734
|
+
return { output: 'Already at root.' };
|
|
735
|
+
}
|
|
736
|
+
|
|
375
737
|
for (let i = 0; i < levels; i++) {
|
|
376
738
|
const parentPath = path.dirname(targetPath);
|
|
377
|
-
|
|
378
|
-
|
|
739
|
+
|
|
740
|
+
if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
|
|
741
|
+
break;
|
|
379
742
|
}
|
|
743
|
+
|
|
380
744
|
targetPath = parentPath;
|
|
745
|
+
|
|
746
|
+
if (targetPath === config.storagePath) {
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
381
749
|
}
|
|
382
750
|
|
|
383
751
|
return { newPath: targetPath, output: '' };
|
|
@@ -441,7 +809,6 @@ export async function processCommand(
|
|
|
441
809
|
fs.renameSync(sourcePath, destPath);
|
|
442
810
|
return { output: `Moved: ${source} -> ${dest}` };
|
|
443
811
|
} catch (error: any) {
|
|
444
|
-
// If rename fails (cross-device), copy then delete
|
|
445
812
|
try {
|
|
446
813
|
copyRecursive(sourcePath, destPath);
|
|
447
814
|
fs.rmSync(sourcePath, { recursive: true, force: true });
|
|
@@ -455,7 +822,6 @@ export async function processCommand(
|
|
|
455
822
|
if (command === 'open') {
|
|
456
823
|
const { exec } = require('child_process');
|
|
457
824
|
|
|
458
|
-
// open or open . - open current folder in system file manager
|
|
459
825
|
if (parts.length < 2 || parts[1] === '.') {
|
|
460
826
|
exec(`open "${currentPath}"`);
|
|
461
827
|
return { output: `Opening: ${currentPath}` };
|
|
@@ -464,18 +830,15 @@ export async function processCommand(
|
|
|
464
830
|
const name = parts.slice(1).join(' ');
|
|
465
831
|
const targetPath = path.join(currentPath, name);
|
|
466
832
|
|
|
467
|
-
// Check if target exists
|
|
468
833
|
if (fs.existsSync(targetPath)) {
|
|
469
834
|
const stat = fs.statSync(targetPath);
|
|
470
835
|
|
|
471
836
|
if (stat.isDirectory()) {
|
|
472
|
-
// It's a folder, open in file manager
|
|
473
837
|
exec(`open "${targetPath}"`);
|
|
474
838
|
return { output: `Opening: ${targetPath}` };
|
|
475
839
|
}
|
|
476
840
|
|
|
477
841
|
if (stat.isFile()) {
|
|
478
|
-
// It's a file, show its content (supports | pbcopy)
|
|
479
842
|
const content = fs.readFileSync(targetPath, 'utf-8');
|
|
480
843
|
return wrapResult({ output: content });
|
|
481
844
|
}
|
|
@@ -531,34 +894,47 @@ export async function processCommand(
|
|
|
531
894
|
|
|
532
895
|
const target = parts.slice(1).join(' ');
|
|
533
896
|
|
|
534
|
-
if (target
|
|
535
|
-
const
|
|
536
|
-
|
|
897
|
+
if (/^\.{2,}$/.test(target)) {
|
|
898
|
+
const levels = target.length - 1;
|
|
899
|
+
let targetPath = currentPath;
|
|
900
|
+
|
|
901
|
+
if (targetPath === config.storagePath) {
|
|
537
902
|
return { output: 'Already at root.' };
|
|
538
903
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
904
|
+
|
|
905
|
+
for (let i = 0; i < levels; i++) {
|
|
906
|
+
const parentPath = path.dirname(targetPath);
|
|
907
|
+
|
|
908
|
+
if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
targetPath = parentPath;
|
|
913
|
+
|
|
914
|
+
if (targetPath === config.storagePath) {
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
547
917
|
}
|
|
918
|
+
|
|
548
919
|
return { newPath: targetPath, output: '' };
|
|
549
920
|
}
|
|
550
921
|
|
|
551
|
-
// Handle paths like "cd bank/account" or "cd ../other"
|
|
552
922
|
if (target.includes('/')) {
|
|
553
923
|
let targetPath = currentPath;
|
|
554
924
|
const pathParts = target.split('/');
|
|
555
925
|
|
|
556
926
|
for (const part of pathParts) {
|
|
557
|
-
if (part
|
|
558
|
-
|
|
927
|
+
if (/^\.{2,}$/.test(part)) {
|
|
928
|
+
const levels = part.length - 1;
|
|
929
|
+
for (let i = 0; i < levels; i++) {
|
|
930
|
+
if (targetPath === config.storagePath) break;
|
|
931
|
+
const parentPath = path.dirname(targetPath);
|
|
932
|
+
if (parentPath.length < config.storagePath.length) break;
|
|
933
|
+
targetPath = parentPath;
|
|
934
|
+
}
|
|
559
935
|
} else if (part === '.') {
|
|
560
936
|
continue;
|
|
561
|
-
} else {
|
|
937
|
+
} else if (part) {
|
|
562
938
|
const newPath = navigateToNode(targetPath, part);
|
|
563
939
|
if (!newPath) {
|
|
564
940
|
return { output: `Path "${target}" not found.` };
|
|
@@ -623,121 +999,69 @@ Storage: ${config.storagePath}
|
|
|
623
999
|
|
|
624
1000
|
if (command === 'help') {
|
|
625
1001
|
return wrapResult({
|
|
626
|
-
output: `
|
|
627
|
-
|
|
628
|
-
ls - List all schemas, todos, and notes
|
|
629
|
-
tree - Show directory tree structure
|
|
630
|
-
tree -A - Show tree with files
|
|
631
|
-
tree --depth=N - Limit tree depth (e.g., --depth=2)
|
|
632
|
-
map - Dungeon map visualization
|
|
633
|
-
cd <node> - Navigate into a node
|
|
634
|
-
cd .. - Go back to parent
|
|
635
|
-
pwd - Show current path
|
|
636
|
-
open - Open current folder in Finder
|
|
637
|
-
open <folder> - Open specific folder in Finder
|
|
638
|
-
mkdir <name> - Create new folder
|
|
639
|
-
cp <src> <dest> - Copy file or folder
|
|
640
|
-
mv <src> <dest> - Move/rename file or folder
|
|
641
|
-
rm <name> - Delete file
|
|
642
|
-
rm -rf <name> - Delete folder recursively
|
|
643
|
-
config - Show configuration
|
|
644
|
-
config:apiKey=<key> - Set API key
|
|
645
|
-
v, version - Show version
|
|
646
|
-
<description> - Create schema/todo (AI generates preview)
|
|
647
|
-
save - Save pending schema to disk
|
|
648
|
-
cancel - Discard pending schema
|
|
649
|
-
clean - Show items to delete in current folder
|
|
650
|
-
clean --yes - Delete all items in current folder
|
|
651
|
-
exit/quit - Exit the program
|
|
1002
|
+
output: `
|
|
1003
|
+
=== ROGUELIKE CLI ===
|
|
652
1004
|
|
|
653
|
-
|
|
654
|
-
ls
|
|
655
|
-
tree
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
4. Type "save" to save or "cancel" to discard
|
|
663
|
-
|
|
664
|
-
Examples:
|
|
1005
|
+
Navigation:
|
|
1006
|
+
ls List tasks and files
|
|
1007
|
+
tree Show task tree with status
|
|
1008
|
+
tree -A Include files
|
|
1009
|
+
tree --depth=N Limit tree depth
|
|
1010
|
+
cd <task> Navigate into task
|
|
1011
|
+
cd .., ... Go back 1 or 2 levels
|
|
1012
|
+
pwd Show current path
|
|
1013
|
+
open Open folder in Finder
|
|
665
1014
|
|
|
666
|
-
|
|
1015
|
+
Task Management:
|
|
1016
|
+
mkdir <name> Create new task
|
|
1017
|
+
done Mark current task as completed (recursive)
|
|
1018
|
+
deadline <date> Set deadline (today, tomorrow, +3d, Jan 15)
|
|
1019
|
+
boss Toggle boss/milestone status (3x XP)
|
|
1020
|
+
block [reason] Mark task as blocked
|
|
1021
|
+
unblock Remove blocked status
|
|
1022
|
+
status Show current task details
|
|
667
1023
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
└── Branch: legal │
|
|
674
|
-
└── open business bank account │
|
|
675
|
-
│ │
|
|
676
|
-
└───────────────────────────────────────────────────────────────┘
|
|
1024
|
+
File Operations:
|
|
1025
|
+
cp <src> <dest> Copy file or folder
|
|
1026
|
+
mv <src> <dest> Move/rename
|
|
1027
|
+
rm <name> Delete file
|
|
1028
|
+
rm -rf <name> Delete folder
|
|
677
1029
|
|
|
678
|
-
|
|
1030
|
+
Gamification:
|
|
1031
|
+
stats Show XP, level, streaks
|
|
1032
|
+
achievements Show achievement list
|
|
1033
|
+
map Dungeon map view
|
|
1034
|
+
map --ai AI-generated dungeon map
|
|
679
1035
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
│ │ back-fastapi │ │ admin-next │ │
|
|
685
|
-
│ │ (VM) │ │ (VM) │ │
|
|
686
|
-
│ └────────┬─────────┘ └──────────────────┘ │
|
|
687
|
-
│ │ │
|
|
688
|
-
│ ├──────────────────┬─────────────────┐ │
|
|
689
|
-
│ │ │ │ │
|
|
690
|
-
│ ┌────────▼────────┐ ┌─────▼──────┐ ┌──────▼────────┐ │
|
|
691
|
-
│ │ PostgreSQL │ │ Redis │ │ Cloudflare │ │
|
|
692
|
-
│ │ (Existing DB) │ │ Cluster │ │ R2 Storage │ │
|
|
693
|
-
│ └─────────────────┘ └────────────┘ └───────────────┘ │
|
|
694
|
-
└─────────────────────────────────────────────────────────────┘
|
|
1036
|
+
Schema Generation:
|
|
1037
|
+
<description> AI generates todo/schema preview
|
|
1038
|
+
save Save pending schema
|
|
1039
|
+
cancel Discard pending schema
|
|
695
1040
|
|
|
696
|
-
|
|
1041
|
+
Utility:
|
|
1042
|
+
init Setup wizard
|
|
1043
|
+
config Show settings
|
|
1044
|
+
clean --yes Clear current folder
|
|
1045
|
+
v, version Show version
|
|
1046
|
+
help This help
|
|
1047
|
+
exit, quit Exit
|
|
697
1048
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
├── web-servers │
|
|
702
|
-
│ ├── app-server-1 │
|
|
703
|
-
│ ├── app-server-2 │
|
|
704
|
-
│ └── app-server-3 │
|
|
705
|
-
├── redis │
|
|
706
|
-
│ ├── cache-cluster │
|
|
707
|
-
│ └── session-store │
|
|
708
|
-
└── database │
|
|
709
|
-
├── postgres-primary │
|
|
710
|
-
└── postgres-replica │
|
|
711
|
-
│ │
|
|
712
|
-
└───────────────────────────────────────────────────────────────┘
|
|
713
|
-
|
|
714
|
-
> kubernetes cluster with clusters postgres and redis
|
|
1049
|
+
Clipboard:
|
|
1050
|
+
<cmd> | pbcopy Copy output (macOS)
|
|
1051
|
+
<cmd> | clip Copy output (Windows)
|
|
715
1052
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
│ │ │ │ │ │
|
|
722
|
-
│ │ primary-pod │ │ cache-pod-1 │ │
|
|
723
|
-
│ │ replica-pod-1│ │ cache-pod-2 │ │
|
|
724
|
-
│ │ replica-pod-2│ │ │ │
|
|
725
|
-
│ └──────┬───────┘ └──────┬───────┘ │
|
|
726
|
-
│ │ │ │
|
|
727
|
-
│ └──────────┬───────────┘ │
|
|
728
|
-
│ │ │
|
|
729
|
-
│ ┌───────▼────────┐ │
|
|
730
|
-
│ │ worker-zones │ │
|
|
731
|
-
│ │ zone-1 │ │
|
|
732
|
-
│ │ zone-2 │ │
|
|
733
|
-
│ └────────────────┘ │
|
|
734
|
-
└─────────────────────────────────────────────────────────────┘
|
|
1053
|
+
Deadlines:
|
|
1054
|
+
deadline today Due today
|
|
1055
|
+
deadline tomorrow Due tomorrow
|
|
1056
|
+
deadline +3d Due in 3 days
|
|
1057
|
+
deadline Jan 15 Due on date
|
|
735
1058
|
|
|
736
|
-
www.rlc.rocks
|
|
1059
|
+
www.rlc.rocks
|
|
1060
|
+
`.trim()
|
|
737
1061
|
});
|
|
738
1062
|
}
|
|
739
1063
|
|
|
740
|
-
// Save command
|
|
1064
|
+
// Save command
|
|
741
1065
|
if (command === 'save') {
|
|
742
1066
|
if (!sessionState.pending) {
|
|
743
1067
|
return wrapResult({ output: 'Nothing to save. Create a schema first.' });
|
|
@@ -747,17 +1071,14 @@ www.rlc.rocks`
|
|
|
747
1071
|
const safeName = pending.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
748
1072
|
|
|
749
1073
|
if (pending.format === 'tree') {
|
|
750
|
-
// Create folder structure from tree ASCII art
|
|
751
1074
|
const rootPath = path.join(currentPath, safeName);
|
|
752
1075
|
createFoldersFromTree(rootPath, pending.content);
|
|
753
1076
|
|
|
754
|
-
// Clear session
|
|
755
1077
|
sessionState.pending = null;
|
|
756
1078
|
sessionState.history = [];
|
|
757
1079
|
|
|
758
1080
|
return wrapResult({ output: `Created todo folder: ${safeName}/` });
|
|
759
1081
|
} else {
|
|
760
|
-
// Save as .rlc.schema file
|
|
761
1082
|
const schemaPath = saveSchemaFile(
|
|
762
1083
|
currentPath,
|
|
763
1084
|
pending.title,
|
|
@@ -765,7 +1086,6 @@ www.rlc.rocks`
|
|
|
765
1086
|
);
|
|
766
1087
|
const filename = path.basename(schemaPath);
|
|
767
1088
|
|
|
768
|
-
// Clear session
|
|
769
1089
|
sessionState.pending = null;
|
|
770
1090
|
sessionState.history = [];
|
|
771
1091
|
|
|
@@ -773,7 +1093,7 @@ www.rlc.rocks`
|
|
|
773
1093
|
}
|
|
774
1094
|
}
|
|
775
1095
|
|
|
776
|
-
// Cancel command
|
|
1096
|
+
// Cancel command
|
|
777
1097
|
if (command === 'cancel') {
|
|
778
1098
|
if (!sessionState.pending) {
|
|
779
1099
|
return wrapResult({ output: 'Nothing to cancel.' });
|
|
@@ -785,7 +1105,7 @@ www.rlc.rocks`
|
|
|
785
1105
|
return wrapResult({ output: 'Discarded pending schema.' });
|
|
786
1106
|
}
|
|
787
1107
|
|
|
788
|
-
// Clean command
|
|
1108
|
+
// Clean command
|
|
789
1109
|
if (command === 'clean') {
|
|
790
1110
|
const entries = fs.readdirSync(currentPath);
|
|
791
1111
|
const toDelete = entries.filter(e => !e.startsWith('.'));
|
|
@@ -794,7 +1114,6 @@ www.rlc.rocks`
|
|
|
794
1114
|
return wrapResult({ output: 'Directory is already empty.' });
|
|
795
1115
|
}
|
|
796
1116
|
|
|
797
|
-
// Check for --yes flag to skip confirmation
|
|
798
1117
|
if (!parts.includes('--yes') && !parts.includes('-y')) {
|
|
799
1118
|
return wrapResult({
|
|
800
1119
|
output: `Will delete ${toDelete.length} items:\n${toDelete.join('\n')}\n\nRun "clean --yes" to confirm.`
|
|
@@ -809,10 +1128,9 @@ www.rlc.rocks`
|
|
|
809
1128
|
return wrapResult({ output: `Deleted ${toDelete.length} items.` });
|
|
810
1129
|
}
|
|
811
1130
|
|
|
812
|
-
// AI generation
|
|
1131
|
+
// AI generation
|
|
813
1132
|
const fullInput = cleanInput;
|
|
814
1133
|
|
|
815
|
-
// Add user message to history
|
|
816
1134
|
sessionState.history.push({ role: 'user', content: fullInput });
|
|
817
1135
|
|
|
818
1136
|
const schema = await generateSchemaWithAI(fullInput, config, signal, sessionState.history);
|
|
@@ -822,7 +1140,6 @@ www.rlc.rocks`
|
|
|
822
1140
|
}
|
|
823
1141
|
|
|
824
1142
|
if (schema) {
|
|
825
|
-
// Store in pending
|
|
826
1143
|
sessionState.pending = {
|
|
827
1144
|
title: schema.title,
|
|
828
1145
|
content: schema.content,
|
|
@@ -830,7 +1147,6 @@ www.rlc.rocks`
|
|
|
830
1147
|
tree: schema.tree
|
|
831
1148
|
};
|
|
832
1149
|
|
|
833
|
-
// Add assistant response to history
|
|
834
1150
|
sessionState.history.push({ role: 'assistant', content: schema.content });
|
|
835
1151
|
|
|
836
1152
|
const safeName = schema.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|