@soulcraft/brainy 3.10.1 → 3.13.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/dist/vfs/PathResolver.d.ts +2 -0
- package/dist/vfs/PathResolver.js +28 -41
- package/dist/vfs/TreeUtils.d.ts +67 -0
- package/dist/vfs/TreeUtils.js +268 -0
- package/dist/vfs/VirtualFileSystem.d.ts +31 -0
- package/dist/vfs/VirtualFileSystem.js +142 -52
- package/dist/vfs/types.d.ts +16 -0
- package/package.json +1 -1
|
@@ -40,10 +40,12 @@ export declare class PathResolver {
|
|
|
40
40
|
private fullResolve;
|
|
41
41
|
/**
|
|
42
42
|
* Resolve a child entity by name within a parent directory
|
|
43
|
+
* Uses proper graph relationships instead of metadata queries
|
|
43
44
|
*/
|
|
44
45
|
private resolveChild;
|
|
45
46
|
/**
|
|
46
47
|
* Get all children of a directory
|
|
48
|
+
* Uses proper graph relationships to traverse the tree
|
|
47
49
|
*/
|
|
48
50
|
getChildren(dirId: string): Promise<VFSEntity[]>;
|
|
49
51
|
/**
|
package/dist/vfs/PathResolver.js
CHANGED
|
@@ -122,69 +122,56 @@ export class PathResolver {
|
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
124
124
|
* Resolve a child entity by name within a parent directory
|
|
125
|
+
* Uses proper graph relationships instead of metadata queries
|
|
125
126
|
*/
|
|
126
127
|
async resolveChild(parentId, name) {
|
|
127
128
|
// Check parent cache first
|
|
128
129
|
const cachedChildren = this.parentCache.get(parentId);
|
|
129
130
|
if (cachedChildren && cachedChildren.has(name)) {
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
const parentPath = parentEntity.metadata.path;
|
|
133
|
-
const childPath = this.joinPath(parentPath, name);
|
|
134
|
-
const pathResults = await this.brain.find({
|
|
135
|
-
where: { path: childPath },
|
|
136
|
-
limit: 1
|
|
137
|
-
});
|
|
138
|
-
if (pathResults.length > 0) {
|
|
139
|
-
return pathResults[0].entity.id;
|
|
140
|
-
}
|
|
131
|
+
// Use cached knowledge to quickly find the child
|
|
132
|
+
// Still need to verify it exists
|
|
141
133
|
}
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const results = await this.brain.find({
|
|
148
|
-
where: { path: childPath },
|
|
149
|
-
limit: 1
|
|
134
|
+
// Use proper graph traversal to find children
|
|
135
|
+
// Get all relationships where parentId contains other entities
|
|
136
|
+
const relations = await this.brain.getRelations({
|
|
137
|
+
from: parentId,
|
|
138
|
+
type: VerbType.Contains
|
|
150
139
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
|
|
140
|
+
// Find the child with matching name
|
|
141
|
+
for (const relation of relations) {
|
|
142
|
+
const childEntity = await this.brain.get(relation.to);
|
|
143
|
+
if (childEntity && childEntity.metadata?.name === name) {
|
|
144
|
+
// Update parent cache
|
|
145
|
+
if (!this.parentCache.has(parentId)) {
|
|
146
|
+
this.parentCache.set(parentId, new Set());
|
|
147
|
+
}
|
|
148
|
+
this.parentCache.get(parentId).add(name);
|
|
149
|
+
return childEntity.id;
|
|
156
150
|
}
|
|
157
|
-
this.parentCache.get(parentId).add(name);
|
|
158
|
-
return childId;
|
|
159
151
|
}
|
|
160
152
|
return null;
|
|
161
153
|
}
|
|
162
154
|
/**
|
|
163
155
|
* Get all children of a directory
|
|
156
|
+
* Uses proper graph relationships to traverse the tree
|
|
164
157
|
*/
|
|
165
158
|
async getChildren(dirId) {
|
|
166
|
-
// Use
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
via: VerbType.Contains
|
|
171
|
-
},
|
|
172
|
-
limit: 10000 // Large limit for directories
|
|
159
|
+
// Use proper graph API to get all Contains relationships from this directory
|
|
160
|
+
const relations = await this.brain.getRelations({
|
|
161
|
+
from: dirId,
|
|
162
|
+
type: VerbType.Contains
|
|
173
163
|
});
|
|
174
|
-
// Filter and process valid VFS entities only
|
|
175
164
|
const validChildren = [];
|
|
176
165
|
const childNames = new Set();
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (entity.metadata?.vfsType &&
|
|
181
|
-
entity.metadata?.name &&
|
|
182
|
-
entity.metadata?.path &&
|
|
183
|
-
entity.id !== dirId) { // Don't include the directory itself
|
|
166
|
+
// Fetch all child entities
|
|
167
|
+
for (const relation of relations) {
|
|
168
|
+
const entity = await this.brain.get(relation.to);
|
|
169
|
+
if (entity && entity.metadata?.vfsType && entity.metadata?.name) {
|
|
184
170
|
validChildren.push(entity);
|
|
185
171
|
childNames.add(entity.metadata.name);
|
|
186
172
|
}
|
|
187
173
|
}
|
|
174
|
+
// Update cache
|
|
188
175
|
this.parentCache.set(dirId, childNames);
|
|
189
176
|
return validChildren;
|
|
190
177
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VFS Tree Utilities
|
|
3
|
+
* Provides safe tree operations that prevent common recursion issues
|
|
4
|
+
*/
|
|
5
|
+
import { VFSEntity } from './types.js';
|
|
6
|
+
export interface TreeNode {
|
|
7
|
+
name: string;
|
|
8
|
+
path: string;
|
|
9
|
+
type: 'file' | 'directory';
|
|
10
|
+
entityId?: string;
|
|
11
|
+
children?: TreeNode[];
|
|
12
|
+
metadata?: any;
|
|
13
|
+
}
|
|
14
|
+
export interface TreeOptions {
|
|
15
|
+
maxDepth?: number;
|
|
16
|
+
includeHidden?: boolean;
|
|
17
|
+
filter?: (node: VFSEntity) => boolean;
|
|
18
|
+
sort?: 'name' | 'modified' | 'size';
|
|
19
|
+
expandAll?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Tree utility functions for VFS
|
|
23
|
+
* These functions ensure proper tree structure without recursion issues
|
|
24
|
+
*/
|
|
25
|
+
export declare class VFSTreeUtils {
|
|
26
|
+
/**
|
|
27
|
+
* Build a safe tree structure from VFS entities
|
|
28
|
+
* Guarantees no directory appears as its own child
|
|
29
|
+
*/
|
|
30
|
+
static buildTree(entities: VFSEntity[], rootPath?: string, options?: TreeOptions): TreeNode;
|
|
31
|
+
/**
|
|
32
|
+
* Get direct children only - guaranteed no self-inclusion
|
|
33
|
+
*/
|
|
34
|
+
static getDirectChildren(entities: VFSEntity[], parentPath: string): VFSEntity[];
|
|
35
|
+
/**
|
|
36
|
+
* Get all descendants (recursive children)
|
|
37
|
+
*/
|
|
38
|
+
static getDescendants(entities: VFSEntity[], ancestorPath: string, includeAncestor?: boolean): VFSEntity[];
|
|
39
|
+
/**
|
|
40
|
+
* Flatten a tree structure back to a list
|
|
41
|
+
*/
|
|
42
|
+
static flattenTree(node: TreeNode): TreeNode[];
|
|
43
|
+
/**
|
|
44
|
+
* Find a node in the tree by path
|
|
45
|
+
*/
|
|
46
|
+
static findNode(root: TreeNode, targetPath: string): TreeNode | null;
|
|
47
|
+
/**
|
|
48
|
+
* Calculate tree statistics
|
|
49
|
+
*/
|
|
50
|
+
static getTreeStats(node: TreeNode): {
|
|
51
|
+
totalNodes: number;
|
|
52
|
+
files: number;
|
|
53
|
+
directories: number;
|
|
54
|
+
maxDepth: number;
|
|
55
|
+
totalSize?: number;
|
|
56
|
+
};
|
|
57
|
+
private static getParentPath;
|
|
58
|
+
private static sortTreeNodes;
|
|
59
|
+
private static limitDepth;
|
|
60
|
+
/**
|
|
61
|
+
* Validate tree structure - ensures no recursion
|
|
62
|
+
*/
|
|
63
|
+
static validateTree(node: TreeNode, visited?: Set<string>): {
|
|
64
|
+
valid: boolean;
|
|
65
|
+
errors: string[];
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VFS Tree Utilities
|
|
3
|
+
* Provides safe tree operations that prevent common recursion issues
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Tree utility functions for VFS
|
|
7
|
+
* These functions ensure proper tree structure without recursion issues
|
|
8
|
+
*/
|
|
9
|
+
export class VFSTreeUtils {
|
|
10
|
+
/**
|
|
11
|
+
* Build a safe tree structure from VFS entities
|
|
12
|
+
* Guarantees no directory appears as its own child
|
|
13
|
+
*/
|
|
14
|
+
static buildTree(entities, rootPath = '/', options = {}) {
|
|
15
|
+
const pathToEntity = new Map();
|
|
16
|
+
const pathToNode = new Map();
|
|
17
|
+
// First pass: index all entities by path
|
|
18
|
+
for (const entity of entities) {
|
|
19
|
+
const path = entity.metadata.path;
|
|
20
|
+
// Critical: Skip if entity IS the root we're building from
|
|
21
|
+
if (path === rootPath) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
pathToEntity.set(path, entity);
|
|
25
|
+
}
|
|
26
|
+
// Create root node
|
|
27
|
+
const rootNode = {
|
|
28
|
+
name: rootPath === '/' ? 'root' : rootPath.split('/').pop(),
|
|
29
|
+
path: rootPath,
|
|
30
|
+
type: 'directory',
|
|
31
|
+
children: []
|
|
32
|
+
};
|
|
33
|
+
pathToNode.set(rootPath, rootNode);
|
|
34
|
+
// Second pass: build tree structure
|
|
35
|
+
const sortedPaths = Array.from(pathToEntity.keys()).sort();
|
|
36
|
+
for (const path of sortedPaths) {
|
|
37
|
+
const entity = pathToEntity.get(path);
|
|
38
|
+
// Apply filter if provided
|
|
39
|
+
if (options.filter && !options.filter(entity)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// Skip hidden files if requested
|
|
43
|
+
if (!options.includeHidden && entity.metadata.name.startsWith('.')) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Create node for this entity
|
|
47
|
+
const node = {
|
|
48
|
+
name: entity.metadata.name,
|
|
49
|
+
path: entity.metadata.path,
|
|
50
|
+
type: entity.metadata.vfsType === 'directory' ? 'directory' : 'file',
|
|
51
|
+
entityId: entity.id,
|
|
52
|
+
metadata: entity.metadata
|
|
53
|
+
};
|
|
54
|
+
if (entity.metadata.vfsType === 'directory') {
|
|
55
|
+
node.children = [];
|
|
56
|
+
}
|
|
57
|
+
pathToNode.set(path, node);
|
|
58
|
+
// Find parent and attach
|
|
59
|
+
const parentPath = this.getParentPath(path);
|
|
60
|
+
const parentNode = pathToNode.get(parentPath);
|
|
61
|
+
if (parentNode && parentNode.children) {
|
|
62
|
+
parentNode.children.push(node);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Sort children if requested
|
|
66
|
+
if (options.sort) {
|
|
67
|
+
this.sortTreeNodes(rootNode, options.sort);
|
|
68
|
+
}
|
|
69
|
+
// Apply depth limit if specified
|
|
70
|
+
if (options.maxDepth !== undefined) {
|
|
71
|
+
this.limitDepth(rootNode, options.maxDepth);
|
|
72
|
+
}
|
|
73
|
+
return rootNode;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get direct children only - guaranteed no self-inclusion
|
|
77
|
+
*/
|
|
78
|
+
static getDirectChildren(entities, parentPath) {
|
|
79
|
+
const children = [];
|
|
80
|
+
const parentDepth = parentPath === '/' ? 0 : parentPath.split('/').length - 1;
|
|
81
|
+
for (const entity of entities) {
|
|
82
|
+
const path = entity.metadata.path;
|
|
83
|
+
// Critical check 1: Skip if this IS the parent
|
|
84
|
+
if (path === parentPath) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// Check if entity is a direct child
|
|
88
|
+
if (path.startsWith(parentPath)) {
|
|
89
|
+
const relativePath = parentPath === '/'
|
|
90
|
+
? path.substring(1)
|
|
91
|
+
: path.substring(parentPath.length + 1);
|
|
92
|
+
// Direct child has no additional slashes
|
|
93
|
+
if (!relativePath.includes('/')) {
|
|
94
|
+
children.push(entity);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return children;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get all descendants (recursive children)
|
|
102
|
+
*/
|
|
103
|
+
static getDescendants(entities, ancestorPath, includeAncestor = false) {
|
|
104
|
+
const descendants = [];
|
|
105
|
+
for (const entity of entities) {
|
|
106
|
+
const path = entity.metadata.path;
|
|
107
|
+
// Include ancestor only if explicitly requested
|
|
108
|
+
if (path === ancestorPath) {
|
|
109
|
+
if (includeAncestor) {
|
|
110
|
+
descendants.push(entity);
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// Check if entity is under ancestor path
|
|
115
|
+
const prefix = ancestorPath === '/' ? '/' : ancestorPath + '/';
|
|
116
|
+
if (path.startsWith(prefix)) {
|
|
117
|
+
descendants.push(entity);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return descendants;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Flatten a tree structure back to a list
|
|
124
|
+
*/
|
|
125
|
+
static flattenTree(node) {
|
|
126
|
+
const result = [node];
|
|
127
|
+
if (node.children) {
|
|
128
|
+
for (const child of node.children) {
|
|
129
|
+
result.push(...this.flattenTree(child));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Find a node in the tree by path
|
|
136
|
+
*/
|
|
137
|
+
static findNode(root, targetPath) {
|
|
138
|
+
if (root.path === targetPath) {
|
|
139
|
+
return root;
|
|
140
|
+
}
|
|
141
|
+
if (root.children) {
|
|
142
|
+
for (const child of root.children) {
|
|
143
|
+
const found = this.findNode(child, targetPath);
|
|
144
|
+
if (found)
|
|
145
|
+
return found;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Calculate tree statistics
|
|
152
|
+
*/
|
|
153
|
+
static getTreeStats(node) {
|
|
154
|
+
let stats = {
|
|
155
|
+
totalNodes: 0,
|
|
156
|
+
files: 0,
|
|
157
|
+
directories: 0,
|
|
158
|
+
maxDepth: 0,
|
|
159
|
+
totalSize: 0
|
|
160
|
+
};
|
|
161
|
+
function traverse(n, depth) {
|
|
162
|
+
stats.totalNodes++;
|
|
163
|
+
stats.maxDepth = Math.max(stats.maxDepth, depth);
|
|
164
|
+
if (n.type === 'file') {
|
|
165
|
+
stats.files++;
|
|
166
|
+
if (n.metadata?.size) {
|
|
167
|
+
stats.totalSize += n.metadata.size;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
stats.directories++;
|
|
172
|
+
}
|
|
173
|
+
if (n.children) {
|
|
174
|
+
for (const child of n.children) {
|
|
175
|
+
traverse(child, depth + 1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
traverse(node, 0);
|
|
180
|
+
return stats;
|
|
181
|
+
}
|
|
182
|
+
// Helper methods
|
|
183
|
+
static getParentPath(path) {
|
|
184
|
+
if (path === '/')
|
|
185
|
+
return '/';
|
|
186
|
+
const parts = path.split('/');
|
|
187
|
+
parts.pop();
|
|
188
|
+
return parts.length === 1 ? '/' : parts.join('/');
|
|
189
|
+
}
|
|
190
|
+
static sortTreeNodes(node, sortBy) {
|
|
191
|
+
if (!node.children)
|
|
192
|
+
return;
|
|
193
|
+
node.children.sort((a, b) => {
|
|
194
|
+
// Directories first, then files
|
|
195
|
+
if (a.type !== b.type) {
|
|
196
|
+
return a.type === 'directory' ? -1 : 1;
|
|
197
|
+
}
|
|
198
|
+
switch (sortBy) {
|
|
199
|
+
case 'name':
|
|
200
|
+
return a.name.localeCompare(b.name);
|
|
201
|
+
case 'modified':
|
|
202
|
+
const aTime = a.metadata?.modified || 0;
|
|
203
|
+
const bTime = b.metadata?.modified || 0;
|
|
204
|
+
return bTime - aTime;
|
|
205
|
+
case 'size':
|
|
206
|
+
const aSize = a.metadata?.size || 0;
|
|
207
|
+
const bSize = b.metadata?.size || 0;
|
|
208
|
+
return bSize - aSize;
|
|
209
|
+
default:
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
// Recursively sort children
|
|
214
|
+
for (const child of node.children) {
|
|
215
|
+
this.sortTreeNodes(child, sortBy);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
static limitDepth(node, maxDepth, currentDepth = 0) {
|
|
219
|
+
if (currentDepth >= maxDepth) {
|
|
220
|
+
delete node.children;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (node.children) {
|
|
224
|
+
for (const child of node.children) {
|
|
225
|
+
this.limitDepth(child, maxDepth, currentDepth + 1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Validate tree structure - ensures no recursion
|
|
231
|
+
*/
|
|
232
|
+
static validateTree(node, visited = new Set()) {
|
|
233
|
+
const errors = [];
|
|
234
|
+
// Check for cycles
|
|
235
|
+
if (visited.has(node.path)) {
|
|
236
|
+
errors.push(`Cycle detected at path: ${node.path}`);
|
|
237
|
+
return { valid: false, errors };
|
|
238
|
+
}
|
|
239
|
+
visited.add(node.path);
|
|
240
|
+
// Check children
|
|
241
|
+
if (node.children) {
|
|
242
|
+
const childPaths = new Set();
|
|
243
|
+
for (const child of node.children) {
|
|
244
|
+
// Check for duplicate children
|
|
245
|
+
if (childPaths.has(child.path)) {
|
|
246
|
+
errors.push(`Duplicate child path: ${child.path}`);
|
|
247
|
+
}
|
|
248
|
+
childPaths.add(child.path);
|
|
249
|
+
// Check child is not parent
|
|
250
|
+
if (child.path === node.path) {
|
|
251
|
+
errors.push(`Directory contains itself: ${node.path}`);
|
|
252
|
+
}
|
|
253
|
+
// Check child is actually under parent
|
|
254
|
+
if (node.path !== '/' && !child.path.startsWith(node.path + '/')) {
|
|
255
|
+
errors.push(`Child ${child.path} not under parent ${node.path}`);
|
|
256
|
+
}
|
|
257
|
+
// Recursively validate children
|
|
258
|
+
const childValidation = this.validateTree(child, new Set(visited));
|
|
259
|
+
errors.push(...childValidation.errors);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
valid: errors.length === 0,
|
|
264
|
+
errors
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=TreeUtils.js.map
|
|
@@ -52,6 +52,37 @@ export declare class VirtualFileSystem implements IVirtualFileSystem {
|
|
|
52
52
|
* Delete a file
|
|
53
53
|
*/
|
|
54
54
|
unlink(path: string): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Get only direct children of a directory - guaranteed no self-inclusion
|
|
57
|
+
* This is the SAFE way to get children for building tree UIs
|
|
58
|
+
*/
|
|
59
|
+
getDirectChildren(path: string): Promise<VFSEntity[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Get a properly structured tree for the given path
|
|
62
|
+
* This prevents recursion issues common when building file explorers
|
|
63
|
+
*/
|
|
64
|
+
getTreeStructure(path: string, options?: {
|
|
65
|
+
maxDepth?: number;
|
|
66
|
+
includeHidden?: boolean;
|
|
67
|
+
sort?: 'name' | 'modified' | 'size';
|
|
68
|
+
}): Promise<any>;
|
|
69
|
+
/**
|
|
70
|
+
* Get all descendants of a directory (flat list)
|
|
71
|
+
*/
|
|
72
|
+
getDescendants(path: string, options?: {
|
|
73
|
+
includeAncestor?: boolean;
|
|
74
|
+
type?: 'file' | 'directory';
|
|
75
|
+
}): Promise<VFSEntity[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Inspect a path and return structured information
|
|
78
|
+
* This is the recommended method for file explorers to use
|
|
79
|
+
*/
|
|
80
|
+
inspect(path: string): Promise<{
|
|
81
|
+
node: VFSEntity;
|
|
82
|
+
children: VFSEntity[];
|
|
83
|
+
parent: VFSEntity | null;
|
|
84
|
+
stats: VFSStats;
|
|
85
|
+
}>;
|
|
55
86
|
/**
|
|
56
87
|
* Create a directory
|
|
57
88
|
*/
|
|
@@ -322,6 +322,116 @@ export class VirtualFileSystem {
|
|
|
322
322
|
this.triggerWatchers(path, 'rename');
|
|
323
323
|
// Knowledge Layer hooks will be added by augmentation if enabled
|
|
324
324
|
}
|
|
325
|
+
// ============= Tree Operations (NEW) =============
|
|
326
|
+
/**
|
|
327
|
+
* Get only direct children of a directory - guaranteed no self-inclusion
|
|
328
|
+
* This is the SAFE way to get children for building tree UIs
|
|
329
|
+
*/
|
|
330
|
+
async getDirectChildren(path) {
|
|
331
|
+
await this.ensureInitialized();
|
|
332
|
+
const entityId = await this.pathResolver.resolve(path);
|
|
333
|
+
const entity = await this.getEntityById(entityId);
|
|
334
|
+
// Verify it's a directory
|
|
335
|
+
if (entity.metadata.vfsType !== 'directory') {
|
|
336
|
+
throw new VFSError(VFSErrorCode.ENOTDIR, `Not a directory: ${path}`, path, 'getDirectChildren');
|
|
337
|
+
}
|
|
338
|
+
// Use the safe getChildren from PathResolver
|
|
339
|
+
const children = await this.pathResolver.getChildren(entityId);
|
|
340
|
+
// Double-check no self-inclusion (paranoid safety)
|
|
341
|
+
return children.filter(child => child.metadata.path !== path);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Get a properly structured tree for the given path
|
|
345
|
+
* This prevents recursion issues common when building file explorers
|
|
346
|
+
*/
|
|
347
|
+
async getTreeStructure(path, options) {
|
|
348
|
+
await this.ensureInitialized();
|
|
349
|
+
const { VFSTreeUtils } = await import('./TreeUtils.js');
|
|
350
|
+
const entityId = await this.pathResolver.resolve(path);
|
|
351
|
+
const entity = await this.getEntityById(entityId);
|
|
352
|
+
if (entity.metadata.vfsType !== 'directory') {
|
|
353
|
+
throw new VFSError(VFSErrorCode.ENOTDIR, `Not a directory: ${path}`, path, 'getTreeStructure');
|
|
354
|
+
}
|
|
355
|
+
// Recursively gather all descendants
|
|
356
|
+
const allEntities = [];
|
|
357
|
+
const visited = new Set();
|
|
358
|
+
const gatherDescendants = async (dirId) => {
|
|
359
|
+
if (visited.has(dirId))
|
|
360
|
+
return; // Prevent cycles
|
|
361
|
+
visited.add(dirId);
|
|
362
|
+
const children = await this.pathResolver.getChildren(dirId);
|
|
363
|
+
for (const child of children) {
|
|
364
|
+
allEntities.push(child);
|
|
365
|
+
if (child.metadata.vfsType === 'directory') {
|
|
366
|
+
await gatherDescendants(child.id);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
await gatherDescendants(entityId);
|
|
371
|
+
// Build safe tree structure
|
|
372
|
+
return VFSTreeUtils.buildTree(allEntities, path, options || {});
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Get all descendants of a directory (flat list)
|
|
376
|
+
*/
|
|
377
|
+
async getDescendants(path, options) {
|
|
378
|
+
await this.ensureInitialized();
|
|
379
|
+
const entityId = await this.pathResolver.resolve(path);
|
|
380
|
+
const entity = await this.getEntityById(entityId);
|
|
381
|
+
if (entity.metadata.vfsType !== 'directory') {
|
|
382
|
+
throw new VFSError(VFSErrorCode.ENOTDIR, `Not a directory: ${path}`, path, 'getDescendants');
|
|
383
|
+
}
|
|
384
|
+
const descendants = [];
|
|
385
|
+
if (options?.includeAncestor) {
|
|
386
|
+
descendants.push(entity);
|
|
387
|
+
}
|
|
388
|
+
const visited = new Set();
|
|
389
|
+
const queue = [entityId];
|
|
390
|
+
while (queue.length > 0) {
|
|
391
|
+
const currentId = queue.shift();
|
|
392
|
+
if (visited.has(currentId))
|
|
393
|
+
continue;
|
|
394
|
+
visited.add(currentId);
|
|
395
|
+
const children = await this.pathResolver.getChildren(currentId);
|
|
396
|
+
for (const child of children) {
|
|
397
|
+
// Filter by type if specified
|
|
398
|
+
if (!options?.type || child.metadata.vfsType === options.type) {
|
|
399
|
+
descendants.push(child);
|
|
400
|
+
}
|
|
401
|
+
// Add directories to queue for traversal
|
|
402
|
+
if (child.metadata.vfsType === 'directory') {
|
|
403
|
+
queue.push(child.id);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return descendants;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Inspect a path and return structured information
|
|
411
|
+
* This is the recommended method for file explorers to use
|
|
412
|
+
*/
|
|
413
|
+
async inspect(path) {
|
|
414
|
+
await this.ensureInitialized();
|
|
415
|
+
const entityId = await this.pathResolver.resolve(path);
|
|
416
|
+
const entity = await this.getEntityById(entityId);
|
|
417
|
+
const stats = await this.stat(path);
|
|
418
|
+
let children = [];
|
|
419
|
+
if (entity.metadata.vfsType === 'directory') {
|
|
420
|
+
children = await this.getDirectChildren(path);
|
|
421
|
+
}
|
|
422
|
+
let parent = null;
|
|
423
|
+
if (path !== '/') {
|
|
424
|
+
const parentPath = path.substring(0, path.lastIndexOf('/')) || '/';
|
|
425
|
+
const parentId = await this.pathResolver.resolve(parentPath);
|
|
426
|
+
parent = await this.getEntityById(parentId);
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
node: entity,
|
|
430
|
+
children,
|
|
431
|
+
parent,
|
|
432
|
+
stats
|
|
433
|
+
};
|
|
434
|
+
}
|
|
325
435
|
// ============= Directory Operations =============
|
|
326
436
|
/**
|
|
327
437
|
* Create a directory
|
|
@@ -1364,37 +1474,29 @@ export class VirtualFileSystem {
|
|
|
1364
1474
|
await this.ensureInitialized();
|
|
1365
1475
|
const entityId = await this.pathResolver.resolve(path);
|
|
1366
1476
|
const results = [];
|
|
1367
|
-
//
|
|
1477
|
+
// Use proper Brainy relationship API to get all relationships
|
|
1368
1478
|
const [fromRelations, toRelations] = await Promise.all([
|
|
1369
|
-
this.brain.
|
|
1370
|
-
|
|
1371
|
-
from: entityId
|
|
1372
|
-
},
|
|
1373
|
-
limit: 1000
|
|
1374
|
-
}),
|
|
1375
|
-
this.brain.find({
|
|
1376
|
-
connected: {
|
|
1377
|
-
to: entityId
|
|
1378
|
-
},
|
|
1379
|
-
limit: 1000
|
|
1380
|
-
})
|
|
1479
|
+
this.brain.getRelations({ from: entityId }),
|
|
1480
|
+
this.brain.getRelations({ to: entityId })
|
|
1381
1481
|
]);
|
|
1382
1482
|
// Add outgoing relationships
|
|
1383
1483
|
for (const rel of fromRelations) {
|
|
1384
|
-
|
|
1484
|
+
const targetEntity = await this.brain.get(rel.to);
|
|
1485
|
+
if (targetEntity && targetEntity.metadata?.path) {
|
|
1385
1486
|
results.push({
|
|
1386
|
-
path:
|
|
1387
|
-
relationship: 'related',
|
|
1487
|
+
path: targetEntity.metadata.path,
|
|
1488
|
+
relationship: rel.type || 'related',
|
|
1388
1489
|
direction: 'from'
|
|
1389
1490
|
});
|
|
1390
1491
|
}
|
|
1391
1492
|
}
|
|
1392
1493
|
// Add incoming relationships
|
|
1393
1494
|
for (const rel of toRelations) {
|
|
1394
|
-
|
|
1495
|
+
const sourceEntity = await this.brain.get(rel.from);
|
|
1496
|
+
if (sourceEntity && sourceEntity.metadata?.path) {
|
|
1395
1497
|
results.push({
|
|
1396
|
-
path:
|
|
1397
|
-
relationship: 'related',
|
|
1498
|
+
path: sourceEntity.metadata.path,
|
|
1499
|
+
relationship: rel.type || 'related',
|
|
1398
1500
|
direction: 'to'
|
|
1399
1501
|
});
|
|
1400
1502
|
}
|
|
@@ -1405,49 +1507,37 @@ export class VirtualFileSystem {
|
|
|
1405
1507
|
await this.ensureInitialized();
|
|
1406
1508
|
const entityId = await this.pathResolver.resolve(path);
|
|
1407
1509
|
const relationships = [];
|
|
1408
|
-
//
|
|
1510
|
+
// Use proper Brainy relationship API
|
|
1409
1511
|
const [fromRelations, toRelations] = await Promise.all([
|
|
1410
|
-
this.brain.
|
|
1411
|
-
|
|
1412
|
-
from: entityId
|
|
1413
|
-
},
|
|
1414
|
-
limit: 1000
|
|
1415
|
-
}),
|
|
1416
|
-
this.brain.find({
|
|
1417
|
-
connected: {
|
|
1418
|
-
to: entityId
|
|
1419
|
-
},
|
|
1420
|
-
limit: 1000
|
|
1421
|
-
})
|
|
1512
|
+
this.brain.getRelations({ from: entityId }),
|
|
1513
|
+
this.brain.getRelations({ to: entityId })
|
|
1422
1514
|
]);
|
|
1423
|
-
//
|
|
1515
|
+
// Process outgoing relationships (excluding Contains for parent-child)
|
|
1424
1516
|
for (const rel of fromRelations) {
|
|
1425
|
-
if (rel.
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
if (parentPath !== path) { // Not a direct child
|
|
1517
|
+
if (rel.type !== VerbType.Contains) { // Skip filesystem hierarchy
|
|
1518
|
+
const targetEntity = await this.brain.get(rel.to);
|
|
1519
|
+
if (targetEntity && targetEntity.metadata?.path) {
|
|
1429
1520
|
relationships.push({
|
|
1430
|
-
id: crypto.randomUUID(),
|
|
1431
|
-
from:
|
|
1432
|
-
to: rel.
|
|
1433
|
-
type:
|
|
1434
|
-
createdAt: Date.now()
|
|
1521
|
+
id: rel.id || crypto.randomUUID(),
|
|
1522
|
+
from: entityId,
|
|
1523
|
+
to: rel.to,
|
|
1524
|
+
type: rel.type,
|
|
1525
|
+
createdAt: rel.createdAt || Date.now()
|
|
1435
1526
|
});
|
|
1436
1527
|
}
|
|
1437
1528
|
}
|
|
1438
1529
|
}
|
|
1439
|
-
//
|
|
1530
|
+
// Process incoming relationships (excluding Contains for parent-child)
|
|
1440
1531
|
for (const rel of toRelations) {
|
|
1441
|
-
if (rel.
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
if (rel.entity.metadata.path !== parentPath) { // Not the parent
|
|
1532
|
+
if (rel.type !== VerbType.Contains) { // Skip filesystem hierarchy
|
|
1533
|
+
const sourceEntity = await this.brain.get(rel.from);
|
|
1534
|
+
if (sourceEntity && sourceEntity.metadata?.path) {
|
|
1445
1535
|
relationships.push({
|
|
1446
|
-
id: crypto.randomUUID(),
|
|
1447
|
-
from: rel.
|
|
1448
|
-
to:
|
|
1449
|
-
type:
|
|
1450
|
-
createdAt: Date.now()
|
|
1536
|
+
id: rel.id || crypto.randomUUID(),
|
|
1537
|
+
from: rel.from,
|
|
1538
|
+
to: entityId,
|
|
1539
|
+
type: rel.type,
|
|
1540
|
+
createdAt: rel.createdAt || Date.now()
|
|
1451
1541
|
});
|
|
1452
1542
|
}
|
|
1453
1543
|
}
|
package/dist/vfs/types.d.ts
CHANGED
|
@@ -283,6 +283,22 @@ export interface IVirtualFileSystem {
|
|
|
283
283
|
recursive?: boolean;
|
|
284
284
|
}): Promise<void>;
|
|
285
285
|
readdir(path: string, options?: ReaddirOptions): Promise<string[] | VFSDirent[]>;
|
|
286
|
+
getDirectChildren(path: string): Promise<VFSEntity[]>;
|
|
287
|
+
getTreeStructure(path: string, options?: {
|
|
288
|
+
maxDepth?: number;
|
|
289
|
+
includeHidden?: boolean;
|
|
290
|
+
sort?: 'name' | 'modified' | 'size';
|
|
291
|
+
}): Promise<any>;
|
|
292
|
+
getDescendants(path: string, options?: {
|
|
293
|
+
includeAncestor?: boolean;
|
|
294
|
+
type?: 'file' | 'directory';
|
|
295
|
+
}): Promise<VFSEntity[]>;
|
|
296
|
+
inspect(path: string): Promise<{
|
|
297
|
+
node: VFSEntity;
|
|
298
|
+
children: VFSEntity[];
|
|
299
|
+
parent: VFSEntity | null;
|
|
300
|
+
stats: VFSStats;
|
|
301
|
+
}>;
|
|
286
302
|
stat(path: string): Promise<VFSStats>;
|
|
287
303
|
lstat(path: string): Promise<VFSStats>;
|
|
288
304
|
exists(path: string): Promise<boolean>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcraft/brainy",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0",
|
|
4
4
|
"description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. 31 nouns × 40 verbs for infinite expressiveness.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|