roguelike-cli 1.2.6 → 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 +383 -181
- 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 +470 -187
- 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,20 +718,18 @@ 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
|
|
728
|
+
// Handle navigation without 'cd' command
|
|
371
729
|
if (/^\.{2,}$/.test(command)) {
|
|
372
|
-
// Count dots: .. = 1 level, ... = 2 levels, .... = 3 levels, etc
|
|
373
730
|
const levels = command.length - 1;
|
|
374
731
|
let targetPath = currentPath;
|
|
375
732
|
|
|
376
|
-
// Already at root?
|
|
377
733
|
if (targetPath === config.storagePath) {
|
|
378
734
|
return { output: 'Already at root.' };
|
|
379
735
|
}
|
|
@@ -381,14 +737,12 @@ export async function processCommand(
|
|
|
381
737
|
for (let i = 0; i < levels; i++) {
|
|
382
738
|
const parentPath = path.dirname(targetPath);
|
|
383
739
|
|
|
384
|
-
// Stop at storage root
|
|
385
740
|
if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
|
|
386
741
|
break;
|
|
387
742
|
}
|
|
388
743
|
|
|
389
744
|
targetPath = parentPath;
|
|
390
745
|
|
|
391
|
-
// If we reached root, stop
|
|
392
746
|
if (targetPath === config.storagePath) {
|
|
393
747
|
break;
|
|
394
748
|
}
|
|
@@ -455,7 +809,6 @@ export async function processCommand(
|
|
|
455
809
|
fs.renameSync(sourcePath, destPath);
|
|
456
810
|
return { output: `Moved: ${source} -> ${dest}` };
|
|
457
811
|
} catch (error: any) {
|
|
458
|
-
// If rename fails (cross-device), copy then delete
|
|
459
812
|
try {
|
|
460
813
|
copyRecursive(sourcePath, destPath);
|
|
461
814
|
fs.rmSync(sourcePath, { recursive: true, force: true });
|
|
@@ -469,7 +822,6 @@ export async function processCommand(
|
|
|
469
822
|
if (command === 'open') {
|
|
470
823
|
const { exec } = require('child_process');
|
|
471
824
|
|
|
472
|
-
// open or open . - open current folder in system file manager
|
|
473
825
|
if (parts.length < 2 || parts[1] === '.') {
|
|
474
826
|
exec(`open "${currentPath}"`);
|
|
475
827
|
return { output: `Opening: ${currentPath}` };
|
|
@@ -478,18 +830,15 @@ export async function processCommand(
|
|
|
478
830
|
const name = parts.slice(1).join(' ');
|
|
479
831
|
const targetPath = path.join(currentPath, name);
|
|
480
832
|
|
|
481
|
-
// Check if target exists
|
|
482
833
|
if (fs.existsSync(targetPath)) {
|
|
483
834
|
const stat = fs.statSync(targetPath);
|
|
484
835
|
|
|
485
836
|
if (stat.isDirectory()) {
|
|
486
|
-
// It's a folder, open in file manager
|
|
487
837
|
exec(`open "${targetPath}"`);
|
|
488
838
|
return { output: `Opening: ${targetPath}` };
|
|
489
839
|
}
|
|
490
840
|
|
|
491
841
|
if (stat.isFile()) {
|
|
492
|
-
// It's a file, show its content (supports | pbcopy)
|
|
493
842
|
const content = fs.readFileSync(targetPath, 'utf-8');
|
|
494
843
|
return wrapResult({ output: content });
|
|
495
844
|
}
|
|
@@ -545,12 +894,10 @@ export async function processCommand(
|
|
|
545
894
|
|
|
546
895
|
const target = parts.slice(1).join(' ');
|
|
547
896
|
|
|
548
|
-
// Handle cd .., cd ..., cd ...., etc
|
|
549
897
|
if (/^\.{2,}$/.test(target)) {
|
|
550
898
|
const levels = target.length - 1;
|
|
551
899
|
let targetPath = currentPath;
|
|
552
900
|
|
|
553
|
-
// Already at root?
|
|
554
901
|
if (targetPath === config.storagePath) {
|
|
555
902
|
return { output: 'Already at root.' };
|
|
556
903
|
}
|
|
@@ -558,14 +905,12 @@ export async function processCommand(
|
|
|
558
905
|
for (let i = 0; i < levels; i++) {
|
|
559
906
|
const parentPath = path.dirname(targetPath);
|
|
560
907
|
|
|
561
|
-
// Stop at storage root
|
|
562
908
|
if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
|
|
563
909
|
break;
|
|
564
910
|
}
|
|
565
911
|
|
|
566
912
|
targetPath = parentPath;
|
|
567
913
|
|
|
568
|
-
// If we reached root, stop
|
|
569
914
|
if (targetPath === config.storagePath) {
|
|
570
915
|
break;
|
|
571
916
|
}
|
|
@@ -574,14 +919,12 @@ export async function processCommand(
|
|
|
574
919
|
return { newPath: targetPath, output: '' };
|
|
575
920
|
}
|
|
576
921
|
|
|
577
|
-
// Handle paths like "cd bank/account" or "cd ../other"
|
|
578
922
|
if (target.includes('/')) {
|
|
579
923
|
let targetPath = currentPath;
|
|
580
924
|
const pathParts = target.split('/');
|
|
581
925
|
|
|
582
926
|
for (const part of pathParts) {
|
|
583
927
|
if (/^\.{2,}$/.test(part)) {
|
|
584
|
-
// Handle .., ..., ...., etc in path
|
|
585
928
|
const levels = part.length - 1;
|
|
586
929
|
for (let i = 0; i < levels; i++) {
|
|
587
930
|
if (targetPath === config.storagePath) break;
|
|
@@ -656,121 +999,69 @@ Storage: ${config.storagePath}
|
|
|
656
999
|
|
|
657
1000
|
if (command === 'help') {
|
|
658
1001
|
return wrapResult({
|
|
659
|
-
output: `
|
|
660
|
-
|
|
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
|
|
1002
|
+
output: `
|
|
1003
|
+
=== ROGUELIKE CLI ===
|
|
685
1004
|
|
|
686
|
-
|
|
687
|
-
ls
|
|
688
|
-
tree
|
|
689
|
-
|
|
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
|
|
690
1014
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
|
696
1023
|
|
|
697
|
-
|
|
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
|
|
698
1029
|
|
|
699
|
-
|
|
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
|
|
700
1035
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
├── get EIN number │
|
|
706
|
-
└── Branch: legal │
|
|
707
|
-
└── open business bank account │
|
|
708
|
-
│ │
|
|
709
|
-
└───────────────────────────────────────────────────────────────┘
|
|
1036
|
+
Schema Generation:
|
|
1037
|
+
<description> AI generates todo/schema preview
|
|
1038
|
+
save Save pending schema
|
|
1039
|
+
cancel Discard pending schema
|
|
710
1040
|
|
|
711
|
-
|
|
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
|
|
712
1048
|
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
└─────────────────────────────────────────────────────────────┘
|
|
728
|
-
|
|
729
|
-
> architecture production redis web application
|
|
730
|
-
|
|
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
|
-
└───────────────────────────────────────────────────────────────┘
|
|
746
|
-
|
|
747
|
-
> kubernetes cluster with clusters postgres and redis
|
|
1049
|
+
Clipboard:
|
|
1050
|
+
<cmd> | pbcopy Copy output (macOS)
|
|
1051
|
+
<cmd> | clip Copy output (Windows)
|
|
748
1052
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
└─────────────────────────────────────────────────────────────┘
|
|
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
|
|
768
1058
|
|
|
769
|
-
www.rlc.rocks
|
|
1059
|
+
www.rlc.rocks
|
|
1060
|
+
`.trim()
|
|
770
1061
|
});
|
|
771
1062
|
}
|
|
772
1063
|
|
|
773
|
-
// Save command
|
|
1064
|
+
// Save command
|
|
774
1065
|
if (command === 'save') {
|
|
775
1066
|
if (!sessionState.pending) {
|
|
776
1067
|
return wrapResult({ output: 'Nothing to save. Create a schema first.' });
|
|
@@ -780,17 +1071,14 @@ www.rlc.rocks`
|
|
|
780
1071
|
const safeName = pending.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
781
1072
|
|
|
782
1073
|
if (pending.format === 'tree') {
|
|
783
|
-
// Create folder structure from tree ASCII art
|
|
784
1074
|
const rootPath = path.join(currentPath, safeName);
|
|
785
1075
|
createFoldersFromTree(rootPath, pending.content);
|
|
786
1076
|
|
|
787
|
-
// Clear session
|
|
788
1077
|
sessionState.pending = null;
|
|
789
1078
|
sessionState.history = [];
|
|
790
1079
|
|
|
791
1080
|
return wrapResult({ output: `Created todo folder: ${safeName}/` });
|
|
792
1081
|
} else {
|
|
793
|
-
// Save as .rlc.schema file
|
|
794
1082
|
const schemaPath = saveSchemaFile(
|
|
795
1083
|
currentPath,
|
|
796
1084
|
pending.title,
|
|
@@ -798,7 +1086,6 @@ www.rlc.rocks`
|
|
|
798
1086
|
);
|
|
799
1087
|
const filename = path.basename(schemaPath);
|
|
800
1088
|
|
|
801
|
-
// Clear session
|
|
802
1089
|
sessionState.pending = null;
|
|
803
1090
|
sessionState.history = [];
|
|
804
1091
|
|
|
@@ -806,7 +1093,7 @@ www.rlc.rocks`
|
|
|
806
1093
|
}
|
|
807
1094
|
}
|
|
808
1095
|
|
|
809
|
-
// Cancel command
|
|
1096
|
+
// Cancel command
|
|
810
1097
|
if (command === 'cancel') {
|
|
811
1098
|
if (!sessionState.pending) {
|
|
812
1099
|
return wrapResult({ output: 'Nothing to cancel.' });
|
|
@@ -818,7 +1105,7 @@ www.rlc.rocks`
|
|
|
818
1105
|
return wrapResult({ output: 'Discarded pending schema.' });
|
|
819
1106
|
}
|
|
820
1107
|
|
|
821
|
-
// Clean command
|
|
1108
|
+
// Clean command
|
|
822
1109
|
if (command === 'clean') {
|
|
823
1110
|
const entries = fs.readdirSync(currentPath);
|
|
824
1111
|
const toDelete = entries.filter(e => !e.startsWith('.'));
|
|
@@ -827,7 +1114,6 @@ www.rlc.rocks`
|
|
|
827
1114
|
return wrapResult({ output: 'Directory is already empty.' });
|
|
828
1115
|
}
|
|
829
1116
|
|
|
830
|
-
// Check for --yes flag to skip confirmation
|
|
831
1117
|
if (!parts.includes('--yes') && !parts.includes('-y')) {
|
|
832
1118
|
return wrapResult({
|
|
833
1119
|
output: `Will delete ${toDelete.length} items:\n${toDelete.join('\n')}\n\nRun "clean --yes" to confirm.`
|
|
@@ -842,10 +1128,9 @@ www.rlc.rocks`
|
|
|
842
1128
|
return wrapResult({ output: `Deleted ${toDelete.length} items.` });
|
|
843
1129
|
}
|
|
844
1130
|
|
|
845
|
-
// AI generation
|
|
1131
|
+
// AI generation
|
|
846
1132
|
const fullInput = cleanInput;
|
|
847
1133
|
|
|
848
|
-
// Add user message to history
|
|
849
1134
|
sessionState.history.push({ role: 'user', content: fullInput });
|
|
850
1135
|
|
|
851
1136
|
const schema = await generateSchemaWithAI(fullInput, config, signal, sessionState.history);
|
|
@@ -855,7 +1140,6 @@ www.rlc.rocks`
|
|
|
855
1140
|
}
|
|
856
1141
|
|
|
857
1142
|
if (schema) {
|
|
858
|
-
// Store in pending
|
|
859
1143
|
sessionState.pending = {
|
|
860
1144
|
title: schema.title,
|
|
861
1145
|
content: schema.content,
|
|
@@ -863,7 +1147,6 @@ www.rlc.rocks`
|
|
|
863
1147
|
tree: schema.tree
|
|
864
1148
|
};
|
|
865
1149
|
|
|
866
|
-
// Add assistant response to history
|
|
867
1150
|
sessionState.history.push({ role: 'assistant', content: schema.content });
|
|
868
1151
|
|
|
869
1152
|
const safeName = schema.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|