@nguyentamdat/mempalace 1.0.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 +127 -0
- package/hooks/README.md +133 -0
- package/hooks/mempal_precompact_hook.sh +35 -0
- package/hooks/mempal_save_hook.sh +80 -0
- package/package.json +36 -0
- package/src/cli.ts +50 -0
- package/src/commands/compress.ts +161 -0
- package/src/commands/init.ts +40 -0
- package/src/commands/mine.ts +51 -0
- package/src/commands/search.ts +23 -0
- package/src/commands/split.ts +20 -0
- package/src/commands/status.ts +12 -0
- package/src/commands/wake-up.ts +20 -0
- package/src/config.ts +111 -0
- package/src/convo-miner.ts +373 -0
- package/src/dialect.ts +921 -0
- package/src/entity-detector.d.ts +25 -0
- package/src/entity-detector.ts +674 -0
- package/src/entity-registry.ts +806 -0
- package/src/general-extractor.ts +487 -0
- package/src/index.ts +5 -0
- package/src/knowledge-graph.ts +461 -0
- package/src/layers.ts +512 -0
- package/src/mcp-server.ts +1034 -0
- package/src/miner.ts +612 -0
- package/src/missing-modules.d.ts +43 -0
- package/src/normalize.ts +374 -0
- package/src/onboarding.ts +485 -0
- package/src/palace-graph.ts +310 -0
- package/src/room-detector-local.ts +415 -0
- package/src/room-detector.d.ts +1 -0
- package/src/room-detector.ts +6 -0
- package/src/searcher.ts +181 -0
- package/src/spellcheck.ts +200 -0
- package/src/split-mega-files.d.ts +8 -0
- package/src/split-mega-files.ts +297 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { ChromaClient } from "chromadb";
|
|
2
|
+
import { MempalaceConfig } from "./config";
|
|
3
|
+
|
|
4
|
+
export interface Node {
|
|
5
|
+
wings: string[];
|
|
6
|
+
halls: string[];
|
|
7
|
+
count: number;
|
|
8
|
+
dates: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Edge {
|
|
12
|
+
room: string;
|
|
13
|
+
wing_a: string;
|
|
14
|
+
wing_b: string;
|
|
15
|
+
hall: string;
|
|
16
|
+
count: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Graph {
|
|
20
|
+
nodes: Record<string, Node>;
|
|
21
|
+
edges: Edge[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type ChromaCollection = {
|
|
25
|
+
count: () => Promise<number>;
|
|
26
|
+
get: (args: {
|
|
27
|
+
limit: number;
|
|
28
|
+
offset: number;
|
|
29
|
+
include: string[];
|
|
30
|
+
}) => Promise<{
|
|
31
|
+
metadatas?: Array<Record<string, unknown> | null>;
|
|
32
|
+
ids?: string[];
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export async function getCollection(
|
|
37
|
+
config: MempalaceConfig = new MempalaceConfig(),
|
|
38
|
+
): Promise<ChromaCollection | null> {
|
|
39
|
+
try {
|
|
40
|
+
const client = new ChromaClient({ path: config.palacePath });
|
|
41
|
+
return (await client.getCollection({
|
|
42
|
+
name: config.collectionName,
|
|
43
|
+
embeddingFunction: undefined as any,
|
|
44
|
+
})) as ChromaCollection;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function buildGraph(
|
|
51
|
+
col: ChromaCollection | null = null,
|
|
52
|
+
config: MempalaceConfig = new MempalaceConfig(),
|
|
53
|
+
): Promise<[Record<string, Node>, Edge[]]> {
|
|
54
|
+
if (col === null) {
|
|
55
|
+
col = await getCollection(config);
|
|
56
|
+
}
|
|
57
|
+
if (!col) {
|
|
58
|
+
return [{}, []];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const total = await col.count();
|
|
62
|
+
const roomData: Record<
|
|
63
|
+
string,
|
|
64
|
+
{ wings: Set<string>; halls: Set<string>; count: number; dates: Set<string> }
|
|
65
|
+
> = {};
|
|
66
|
+
|
|
67
|
+
let offset = 0;
|
|
68
|
+
while (offset < total) {
|
|
69
|
+
const batch = await col.get({ limit: 1000, offset, include: ["metadatas"] });
|
|
70
|
+
for (const meta of batch.metadatas ?? []) {
|
|
71
|
+
if (!meta) continue;
|
|
72
|
+
const room = String(meta.room ?? "");
|
|
73
|
+
const wing = String(meta.wing ?? "");
|
|
74
|
+
const hall = String(meta.hall ?? "");
|
|
75
|
+
const date = String(meta.date ?? "");
|
|
76
|
+
|
|
77
|
+
if (room && room !== "general" && wing) {
|
|
78
|
+
roomData[room] ??= {
|
|
79
|
+
wings: new Set<string>(),
|
|
80
|
+
halls: new Set<string>(),
|
|
81
|
+
count: 0,
|
|
82
|
+
dates: new Set<string>(),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
roomData[room].wings.add(wing);
|
|
86
|
+
if (hall) roomData[room].halls.add(hall);
|
|
87
|
+
if (date) roomData[room].dates.add(date);
|
|
88
|
+
roomData[room].count += 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!batch.ids || batch.ids.length === 0) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
offset += batch.ids.length;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const edges: Edge[] = [];
|
|
99
|
+
for (const [room, data] of Object.entries(roomData)) {
|
|
100
|
+
const wings = [...data.wings].sort();
|
|
101
|
+
if (wings.length >= 2) {
|
|
102
|
+
for (let i = 0; i < wings.length; i += 1) {
|
|
103
|
+
const wa = wings[i];
|
|
104
|
+
for (let j = i + 1; j < wings.length; j += 1) {
|
|
105
|
+
const wb = wings[j];
|
|
106
|
+
for (const hall of data.halls) {
|
|
107
|
+
edges.push({
|
|
108
|
+
room,
|
|
109
|
+
wing_a: wa,
|
|
110
|
+
wing_b: wb,
|
|
111
|
+
hall,
|
|
112
|
+
count: data.count,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const nodes: Record<string, Node> = {};
|
|
121
|
+
for (const [room, data] of Object.entries(roomData)) {
|
|
122
|
+
const dates = [...data.dates].sort();
|
|
123
|
+
nodes[room] = {
|
|
124
|
+
wings: [...data.wings].sort(),
|
|
125
|
+
halls: [...data.halls].sort(),
|
|
126
|
+
count: data.count,
|
|
127
|
+
dates: dates.length ? dates.slice(-5) : [],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return [nodes, edges];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function traverse(
|
|
135
|
+
startRoom: string,
|
|
136
|
+
col: ChromaCollection | null = null,
|
|
137
|
+
config: MempalaceConfig = new MempalaceConfig(),
|
|
138
|
+
maxHops = 2,
|
|
139
|
+
): Promise<
|
|
140
|
+
| Array<{
|
|
141
|
+
room: string;
|
|
142
|
+
wings: string[];
|
|
143
|
+
halls: string[];
|
|
144
|
+
count: number;
|
|
145
|
+
hop: number;
|
|
146
|
+
connected_via?: string[];
|
|
147
|
+
}>
|
|
148
|
+
| { error: string; suggestions: string[] }
|
|
149
|
+
> {
|
|
150
|
+
const [nodes] = await buildGraph(col, config);
|
|
151
|
+
|
|
152
|
+
if (!(startRoom in nodes)) {
|
|
153
|
+
return {
|
|
154
|
+
error: `Room '${startRoom}' not found`,
|
|
155
|
+
suggestions: fuzzyMatch(startRoom, nodes),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const start = nodes[startRoom];
|
|
160
|
+
const visited = new Set<string>([startRoom]);
|
|
161
|
+
const results: Array<{
|
|
162
|
+
room: string;
|
|
163
|
+
wings: string[];
|
|
164
|
+
halls: string[];
|
|
165
|
+
count: number;
|
|
166
|
+
hop: number;
|
|
167
|
+
connected_via?: string[];
|
|
168
|
+
}> = [
|
|
169
|
+
{
|
|
170
|
+
room: startRoom,
|
|
171
|
+
wings: start.wings,
|
|
172
|
+
halls: start.halls,
|
|
173
|
+
count: start.count,
|
|
174
|
+
hop: 0,
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
const frontier: Array<[string, number]> = [[startRoom, 0]];
|
|
179
|
+
while (frontier.length > 0) {
|
|
180
|
+
const [currentRoom, depth] = frontier.shift() as [string, number];
|
|
181
|
+
if (depth >= maxHops) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const current = nodes[currentRoom] ?? { wings: [], halls: [], count: 0, dates: [] };
|
|
186
|
+
const currentWings = new Set(current.wings);
|
|
187
|
+
|
|
188
|
+
for (const [room, data] of Object.entries(nodes)) {
|
|
189
|
+
if (visited.has(room)) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const sharedWings = intersectSets(currentWings, new Set(data.wings));
|
|
194
|
+
if (sharedWings.length > 0) {
|
|
195
|
+
visited.add(room);
|
|
196
|
+
results.push({
|
|
197
|
+
room,
|
|
198
|
+
wings: data.wings,
|
|
199
|
+
halls: data.halls,
|
|
200
|
+
count: data.count,
|
|
201
|
+
hop: depth + 1,
|
|
202
|
+
connected_via: sharedWings.sort(),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (depth + 1 < maxHops) {
|
|
206
|
+
frontier.push([room, depth + 1]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
results.sort((a, b) => a.hop - b.hop || b.count - a.count);
|
|
213
|
+
return results.slice(0, 50);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function findTunnels(
|
|
217
|
+
wingA: string | null = null,
|
|
218
|
+
wingB: string | null = null,
|
|
219
|
+
col: ChromaCollection | null = null,
|
|
220
|
+
config: MempalaceConfig = new MempalaceConfig(),
|
|
221
|
+
): Promise<Array<{ room: string; wings: string[]; halls: string[]; count: number; recent: string }>> {
|
|
222
|
+
const [nodes] = await buildGraph(col, config);
|
|
223
|
+
|
|
224
|
+
const tunnels: Array<{ room: string; wings: string[]; halls: string[]; count: number; recent: string }> = [];
|
|
225
|
+
for (const [room, data] of Object.entries(nodes)) {
|
|
226
|
+
const wings = data.wings;
|
|
227
|
+
if (wings.length < 2) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (wingA && !wings.includes(wingA)) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (wingB && !wings.includes(wingB)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
tunnels.push({
|
|
238
|
+
room,
|
|
239
|
+
wings,
|
|
240
|
+
halls: data.halls,
|
|
241
|
+
count: data.count,
|
|
242
|
+
recent: data.dates.length ? data.dates[data.dates.length - 1] : "",
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
tunnels.sort((a, b) => b.count - a.count);
|
|
247
|
+
return tunnels.slice(0, 50);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function graphStats(
|
|
251
|
+
col: ChromaCollection | null = null,
|
|
252
|
+
config: MempalaceConfig = new MempalaceConfig(),
|
|
253
|
+
): Promise<{
|
|
254
|
+
total_rooms: number;
|
|
255
|
+
tunnel_rooms: number;
|
|
256
|
+
total_edges: number;
|
|
257
|
+
rooms_per_wing: Record<string, number>;
|
|
258
|
+
top_tunnels: Array<{ room: string; wings: string[]; count: number }>;
|
|
259
|
+
}> {
|
|
260
|
+
const [nodes, edges] = await buildGraph(col, config);
|
|
261
|
+
|
|
262
|
+
let tunnelRooms = 0;
|
|
263
|
+
const wingCounts = new Map<string, number>();
|
|
264
|
+
for (const data of Object.values(nodes)) {
|
|
265
|
+
if (data.wings.length >= 2) {
|
|
266
|
+
tunnelRooms += 1;
|
|
267
|
+
}
|
|
268
|
+
for (const wing of data.wings) {
|
|
269
|
+
wingCounts.set(wing, (wingCounts.get(wing) ?? 0) + 1);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
total_rooms: Object.keys(nodes).length,
|
|
275
|
+
tunnel_rooms: tunnelRooms,
|
|
276
|
+
total_edges: edges.length,
|
|
277
|
+
rooms_per_wing: Object.fromEntries([...wingCounts.entries()].sort((a, b) => b[1] - a[1])),
|
|
278
|
+
top_tunnels: Object.entries(nodes)
|
|
279
|
+
.sort((a, b) => b[1].wings.length - a[1].wings.length)
|
|
280
|
+
.slice(0, 10)
|
|
281
|
+
.filter(([, data]) => data.wings.length >= 2)
|
|
282
|
+
.map(([room, data]) => ({ room, wings: data.wings, count: data.count })),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function fuzzyMatch(query: string, nodes: Record<string, Node>, n = 5): string[] {
|
|
287
|
+
const queryLower = query.toLowerCase();
|
|
288
|
+
const scored: Array<[string, number]> = [];
|
|
289
|
+
|
|
290
|
+
for (const room of Object.keys(nodes)) {
|
|
291
|
+
if (queryLower.includes(room)) {
|
|
292
|
+
scored.push([room, 1.0]);
|
|
293
|
+
} else if (queryLower.split("-").some((word) => word && room.includes(word))) {
|
|
294
|
+
scored.push([room, 0.5]);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
scored.sort((a, b) => b[1] - a[1]);
|
|
299
|
+
return scored.slice(0, n).map(([room]) => room);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function intersectSets(left: Set<string>, right: Set<string>): string[] {
|
|
303
|
+
const shared: string[] = [];
|
|
304
|
+
for (const value of left) {
|
|
305
|
+
if (right.has(value)) {
|
|
306
|
+
shared.push(value);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return shared;
|
|
310
|
+
}
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { cancel, confirm, isCancel, select, text } from "@clack/prompts";
|
|
2
|
+
import { readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import { scanProject } from "./miner";
|
|
6
|
+
|
|
7
|
+
export type Room = {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
keywords: string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const FOLDER_ROOM_MAP: Record<string, string> = {
|
|
14
|
+
frontend: "frontend",
|
|
15
|
+
"front-end": "frontend",
|
|
16
|
+
front_end: "frontend",
|
|
17
|
+
client: "frontend",
|
|
18
|
+
ui: "frontend",
|
|
19
|
+
views: "frontend",
|
|
20
|
+
components: "frontend",
|
|
21
|
+
pages: "frontend",
|
|
22
|
+
backend: "backend",
|
|
23
|
+
"back-end": "backend",
|
|
24
|
+
back_end: "backend",
|
|
25
|
+
server: "backend",
|
|
26
|
+
api: "backend",
|
|
27
|
+
routes: "backend",
|
|
28
|
+
services: "backend",
|
|
29
|
+
controllers: "backend",
|
|
30
|
+
models: "backend",
|
|
31
|
+
database: "backend",
|
|
32
|
+
db: "backend",
|
|
33
|
+
docs: "documentation",
|
|
34
|
+
doc: "documentation",
|
|
35
|
+
documentation: "documentation",
|
|
36
|
+
wiki: "documentation",
|
|
37
|
+
readme: "documentation",
|
|
38
|
+
notes: "documentation",
|
|
39
|
+
design: "design",
|
|
40
|
+
designs: "design",
|
|
41
|
+
mockups: "design",
|
|
42
|
+
wireframes: "design",
|
|
43
|
+
assets: "design",
|
|
44
|
+
storyboard: "design",
|
|
45
|
+
costs: "costs",
|
|
46
|
+
cost: "costs",
|
|
47
|
+
budget: "costs",
|
|
48
|
+
finance: "costs",
|
|
49
|
+
financial: "costs",
|
|
50
|
+
pricing: "costs",
|
|
51
|
+
invoices: "costs",
|
|
52
|
+
accounting: "costs",
|
|
53
|
+
meetings: "meetings",
|
|
54
|
+
meeting: "meetings",
|
|
55
|
+
calls: "meetings",
|
|
56
|
+
meeting_notes: "meetings",
|
|
57
|
+
standup: "meetings",
|
|
58
|
+
minutes: "meetings",
|
|
59
|
+
team: "team",
|
|
60
|
+
staff: "team",
|
|
61
|
+
hr: "team",
|
|
62
|
+
hiring: "team",
|
|
63
|
+
employees: "team",
|
|
64
|
+
people: "team",
|
|
65
|
+
research: "research",
|
|
66
|
+
references: "research",
|
|
67
|
+
reading: "research",
|
|
68
|
+
papers: "research",
|
|
69
|
+
planning: "planning",
|
|
70
|
+
roadmap: "planning",
|
|
71
|
+
strategy: "planning",
|
|
72
|
+
specs: "planning",
|
|
73
|
+
requirements: "planning",
|
|
74
|
+
tests: "testing",
|
|
75
|
+
test: "testing",
|
|
76
|
+
testing: "testing",
|
|
77
|
+
qa: "testing",
|
|
78
|
+
scripts: "scripts",
|
|
79
|
+
tools: "scripts",
|
|
80
|
+
utils: "scripts",
|
|
81
|
+
config: "configuration",
|
|
82
|
+
configs: "configuration",
|
|
83
|
+
settings: "configuration",
|
|
84
|
+
infrastructure: "configuration",
|
|
85
|
+
infra: "configuration",
|
|
86
|
+
deploy: "configuration",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const SKIP_DIRS = new Set([
|
|
90
|
+
".git",
|
|
91
|
+
"node_modules",
|
|
92
|
+
"__pycache__",
|
|
93
|
+
".venv",
|
|
94
|
+
"venv",
|
|
95
|
+
"env",
|
|
96
|
+
"dist",
|
|
97
|
+
"build",
|
|
98
|
+
".next",
|
|
99
|
+
"coverage",
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
const FILE_FALLBACK_SKIP_DIRS = new Set([
|
|
103
|
+
".git",
|
|
104
|
+
"node_modules",
|
|
105
|
+
"__pycache__",
|
|
106
|
+
".venv",
|
|
107
|
+
"venv",
|
|
108
|
+
"dist",
|
|
109
|
+
"build",
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
function normalizeName(value: string): string {
|
|
113
|
+
return value.toLowerCase().replace(/-/g, "_").replace(/ /g, "_");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatRule(char: string, width = 55): string {
|
|
117
|
+
return char.repeat(width);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function ensureNotCancelled<T>(value: T): T {
|
|
121
|
+
if (isCancel(value)) {
|
|
122
|
+
cancel("Operation cancelled.");
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function promptText(message: string, placeholder?: string, initialValue?: string): Promise<string> {
|
|
130
|
+
return ensureNotCancelled(
|
|
131
|
+
await text({
|
|
132
|
+
message,
|
|
133
|
+
placeholder,
|
|
134
|
+
initialValue,
|
|
135
|
+
}),
|
|
136
|
+
) as string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function promptConfirm(message: string, initialValue = false): Promise<boolean> {
|
|
140
|
+
return ensureNotCancelled(
|
|
141
|
+
await confirm({
|
|
142
|
+
message,
|
|
143
|
+
initialValue,
|
|
144
|
+
}),
|
|
145
|
+
) as boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function promptChoice(): Promise<"accept" | "edit" | "add"> {
|
|
149
|
+
return ensureNotCancelled(
|
|
150
|
+
await select<"accept" | "edit" | "add">({
|
|
151
|
+
message: "Choose how to proceed",
|
|
152
|
+
options: [
|
|
153
|
+
{ value: "accept", label: "Accept all rooms" },
|
|
154
|
+
{ value: "edit", label: "Edit existing rooms" },
|
|
155
|
+
{ value: "add", label: "Add rooms manually" },
|
|
156
|
+
],
|
|
157
|
+
}),
|
|
158
|
+
) as "accept" | "edit" | "add";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildRoom(name: string, description: string, keywords: string[]): Room {
|
|
162
|
+
return { name, description, keywords };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function detectRoomsFromFolders(projectDir: string): Room[] {
|
|
166
|
+
const projectPath = resolve(projectDir);
|
|
167
|
+
const foundRooms = new Map<string, string>();
|
|
168
|
+
|
|
169
|
+
for (const item of readdirSync(projectPath, { withFileTypes: true })) {
|
|
170
|
+
if (!item.isDirectory() || SKIP_DIRS.has(item.name)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const normalized = item.name.toLowerCase().replace(/-/g, "_");
|
|
175
|
+
const mappedRoom = FOLDER_ROOM_MAP[normalized];
|
|
176
|
+
if (mappedRoom) {
|
|
177
|
+
if (!foundRooms.has(mappedRoom)) {
|
|
178
|
+
foundRooms.set(mappedRoom, item.name);
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (item.name.length > 2 && /^[A-Za-z]/.test(item.name)) {
|
|
184
|
+
const clean = normalizeName(item.name);
|
|
185
|
+
if (!foundRooms.has(clean)) {
|
|
186
|
+
foundRooms.set(clean, item.name);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const item of readdirSync(projectPath, { withFileTypes: true })) {
|
|
192
|
+
if (!item.isDirectory() || SKIP_DIRS.has(item.name)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const itemPath = resolve(projectPath, item.name);
|
|
197
|
+
for (const subitem of readdirSync(itemPath, { withFileTypes: true })) {
|
|
198
|
+
if (!subitem.isDirectory() || SKIP_DIRS.has(subitem.name)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const normalized = subitem.name.toLowerCase().replace(/-/g, "_");
|
|
203
|
+
const mappedRoom = FOLDER_ROOM_MAP[normalized];
|
|
204
|
+
if (mappedRoom && !foundRooms.has(mappedRoom)) {
|
|
205
|
+
foundRooms.set(mappedRoom, subitem.name);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const rooms = Array.from(foundRooms.entries(), ([roomName, original]) =>
|
|
211
|
+
buildRoom(roomName, `Files from ${original}/`, [roomName, original.toLowerCase()]),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (!rooms.some((room) => room.name === "general")) {
|
|
215
|
+
rooms.push(buildRoom("general", "Files that don't fit other rooms", []));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return rooms;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function detectRoomsFromFiles(projectDir: string): Room[] {
|
|
222
|
+
const projectPath = resolve(projectDir);
|
|
223
|
+
const keywordCounts = new Map<string, number>();
|
|
224
|
+
|
|
225
|
+
const walk = (currentDir: string): void => {
|
|
226
|
+
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
|
227
|
+
const entryPath = resolve(currentDir, entry.name);
|
|
228
|
+
|
|
229
|
+
if (entry.isDirectory()) {
|
|
230
|
+
if (!FILE_FALLBACK_SKIP_DIRS.has(entry.name)) {
|
|
231
|
+
walk(entryPath);
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!entry.isFile()) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const normalizedName = normalizeName(entry.name);
|
|
241
|
+
for (const [keyword, room] of Object.entries(FOLDER_ROOM_MAP)) {
|
|
242
|
+
if (normalizedName.includes(keyword)) {
|
|
243
|
+
keywordCounts.set(room, (keywordCounts.get(room) ?? 0) + 1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
walk(projectPath);
|
|
250
|
+
|
|
251
|
+
const rooms: Room[] = [];
|
|
252
|
+
for (const [room, count] of [...keywordCounts.entries()].sort((left, right) => right[1] - left[1])) {
|
|
253
|
+
if (count >= 2) {
|
|
254
|
+
rooms.push(buildRoom(room, `Files related to ${room}`, [room]));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (rooms.length >= 6) {
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (rooms.length === 0) {
|
|
263
|
+
return [buildRoom("general", "All project files", [])];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return rooms;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function printProposedStructure(projectName: string, rooms: Room[], totalFiles: number, source: string): void {
|
|
270
|
+
console.log(`\n${formatRule("=")}`);
|
|
271
|
+
console.log(" MemPalace Init — Local setup");
|
|
272
|
+
console.log(formatRule("="));
|
|
273
|
+
console.log(`\n WING: ${projectName}`);
|
|
274
|
+
console.log(` (${totalFiles} files found, rooms detected from ${source})\n`);
|
|
275
|
+
for (const room of rooms) {
|
|
276
|
+
console.log(` ROOM: ${room.name}`);
|
|
277
|
+
console.log(` ${room.description}`);
|
|
278
|
+
}
|
|
279
|
+
console.log(`\n${formatRule("─")}\n`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function getUserApproval(rooms: Room[]): Promise<Room[]> {
|
|
283
|
+
console.log(" Review the proposed rooms above.");
|
|
284
|
+
|
|
285
|
+
const choice = await promptChoice();
|
|
286
|
+
|
|
287
|
+
let nextRooms = [...rooms];
|
|
288
|
+
|
|
289
|
+
if (choice === "edit") {
|
|
290
|
+
console.log("\n Current rooms:");
|
|
291
|
+
nextRooms.forEach((room, index) => {
|
|
292
|
+
console.log(` ${index + 1}. ${room.name} — ${room.description}`);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const removeInput = await promptText(
|
|
296
|
+
"Room numbers to remove (comma-separated, leave blank to skip)",
|
|
297
|
+
"2,4",
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (removeInput.trim()) {
|
|
301
|
+
const toRemove = new Set(
|
|
302
|
+
removeInput
|
|
303
|
+
.split(",")
|
|
304
|
+
.map((value: string) => Number.parseInt(value.trim(), 10) - 1)
|
|
305
|
+
.filter((value: number) => Number.isInteger(value) && value >= 0),
|
|
306
|
+
);
|
|
307
|
+
nextRooms = nextRooms.filter((_, index) => !toRemove.has(index));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (nextRooms.length > 0) {
|
|
311
|
+
const shouldRename = await promptConfirm("Rename any remaining rooms?");
|
|
312
|
+
|
|
313
|
+
if (shouldRename) {
|
|
314
|
+
const renamedRooms: Room[] = [];
|
|
315
|
+
for (const room of nextRooms) {
|
|
316
|
+
const newName = await promptText(
|
|
317
|
+
`Rename '${room.name}' (leave blank to keep)`,
|
|
318
|
+
room.name,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const trimmedName = newName.trim();
|
|
322
|
+
if (!trimmedName) {
|
|
323
|
+
renamedRooms.push(room);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const normalizedRoomName = normalizeName(trimmedName);
|
|
328
|
+
const newDescription = await promptText(
|
|
329
|
+
`Description for '${normalizedRoomName}'`,
|
|
330
|
+
undefined,
|
|
331
|
+
room.description,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
renamedRooms.push(
|
|
335
|
+
buildRoom(normalizedRoomName, newDescription.trim() || room.description, [normalizedRoomName]),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
nextRooms = renamedRooms;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const wantsAdd = choice === "add";
|
|
345
|
+
if (wantsAdd || (await promptConfirm("Add any missing rooms?", wantsAdd))) {
|
|
346
|
+
while (true) {
|
|
347
|
+
const newName = await promptText(
|
|
348
|
+
"New room name (leave blank to stop)",
|
|
349
|
+
"research_notes",
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const normalizedRoomName = normalizeName(newName.trim());
|
|
353
|
+
if (!normalizedRoomName) {
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const newDescription = await promptText(`Description for '${normalizedRoomName}'`);
|
|
358
|
+
|
|
359
|
+
nextRooms.push(buildRoom(normalizedRoomName, newDescription.trim(), [normalizedRoomName]));
|
|
360
|
+
console.log(` Added: ${normalizedRoomName}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return nextRooms;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function saveConfig(projectDir: string, projectName: string, rooms: Room[]): void {
|
|
368
|
+
const config = {
|
|
369
|
+
wing: projectName,
|
|
370
|
+
rooms: rooms.map((room) => ({
|
|
371
|
+
name: room.name,
|
|
372
|
+
description: room.description,
|
|
373
|
+
})),
|
|
374
|
+
};
|
|
375
|
+
const configPath = resolve(projectDir, "mempalace.yaml");
|
|
376
|
+
writeFileSync(configPath, yaml.dump(config), "utf-8");
|
|
377
|
+
|
|
378
|
+
console.log(`\n Config saved: ${configPath}`);
|
|
379
|
+
console.log("\n Next step:");
|
|
380
|
+
console.log(` mempalace mine ${projectDir}`);
|
|
381
|
+
console.log(`\n${formatRule("=")}\n`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function detectRoomsLocal(projectDir: string): Promise<void> {
|
|
385
|
+
const projectPath = resolve(projectDir);
|
|
386
|
+
const projectName = basename(projectPath).toLowerCase().replace(/ /g, "_").replace(/-/g, "_");
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
if (!statSync(projectPath).isDirectory()) {
|
|
390
|
+
throw new Error("Not a directory");
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
console.log(`ERROR: Directory not found: ${projectDir}`);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const files = scanProject(projectDir);
|
|
398
|
+
|
|
399
|
+
let rooms = detectRoomsFromFolders(projectDir);
|
|
400
|
+
let source = "folder structure";
|
|
401
|
+
|
|
402
|
+
if (rooms.length <= 1) {
|
|
403
|
+
rooms = detectRoomsFromFiles(projectDir);
|
|
404
|
+
source = "filename patterns";
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (rooms.length === 0) {
|
|
408
|
+
rooms = [buildRoom("general", "All project files", [])];
|
|
409
|
+
source = "fallback (flat project)";
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
printProposedStructure(projectName, rooms, files.length, source);
|
|
413
|
+
const approvedRooms = await getUserApproval(rooms);
|
|
414
|
+
saveConfig(projectDir, projectName, approvedRooms);
|
|
415
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function detectRoomsLocal(dir: string): Promise<void>;
|