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