@soederpop/luca 0.0.28 → 0.0.30

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.
Files changed (51) hide show
  1. package/commands/try-all-challenges.ts +1 -1
  2. package/docs/TABLE-OF-CONTENTS.md +0 -3
  3. package/docs/examples/structured-output-with-assistants.md +144 -0
  4. package/docs/tutorials/20-browser-esm.md +234 -0
  5. package/package.json +1 -1
  6. package/src/agi/container.server.ts +4 -0
  7. package/src/agi/features/assistant.ts +132 -2
  8. package/src/agi/features/browser-use.ts +623 -0
  9. package/src/agi/features/conversation.ts +135 -45
  10. package/src/agi/lib/interceptor-chain.ts +79 -0
  11. package/src/bootstrap/generated.ts +381 -308
  12. package/src/cli/build-info.ts +2 -2
  13. package/src/clients/rest.ts +7 -7
  14. package/src/commands/chat.ts +22 -0
  15. package/src/commands/describe.ts +67 -2
  16. package/src/commands/prompt.ts +23 -3
  17. package/src/container.ts +411 -113
  18. package/src/helper.ts +189 -5
  19. package/src/introspection/generated.agi.ts +17664 -11568
  20. package/src/introspection/generated.node.ts +4891 -1860
  21. package/src/introspection/generated.web.ts +379 -291
  22. package/src/introspection/index.ts +7 -0
  23. package/src/introspection/scan.ts +224 -7
  24. package/src/node/container.ts +31 -10
  25. package/src/node/features/content-db.ts +7 -7
  26. package/src/node/features/disk-cache.ts +11 -11
  27. package/src/node/features/esbuild.ts +3 -3
  28. package/src/node/features/file-manager.ts +37 -16
  29. package/src/node/features/fs.ts +64 -25
  30. package/src/node/features/git.ts +10 -10
  31. package/src/node/features/helpers.ts +25 -18
  32. package/src/node/features/ink.ts +13 -13
  33. package/src/node/features/ipc-socket.ts +8 -8
  34. package/src/node/features/networking.ts +3 -3
  35. package/src/node/features/os.ts +7 -7
  36. package/src/node/features/package-finder.ts +15 -15
  37. package/src/node/features/proc.ts +1 -1
  38. package/src/node/features/ui.ts +13 -13
  39. package/src/node/features/vm.ts +4 -4
  40. package/src/scaffolds/generated.ts +1 -1
  41. package/src/servers/express.ts +6 -6
  42. package/src/servers/mcp.ts +4 -4
  43. package/src/servers/socket.ts +6 -6
  44. package/test/interceptor-chain.test.ts +61 -0
  45. package/docs/apis/features/node/window-manager.md +0 -445
  46. package/docs/examples/window-manager-layouts.md +0 -180
  47. package/docs/examples/window-manager.md +0 -125
  48. package/docs/window-manager-fix.md +0 -249
  49. package/scripts/test-window-manager-lifecycle.ts +0 -86
  50. package/scripts/test-window-manager.ts +0 -43
  51. package/src/node/features/window-manager.ts +0 -1603
@@ -3,7 +3,7 @@ import { FeatureEventsSchema, FeatureStateSchema, FeatureOptionsSchema } from '.
3
3
  import { State } from "../../state.js";
4
4
  import { Feature } from "../feature.js";
5
5
  import { parse, relative, join as pathJoin } from "path";
6
- import { statSync, readFileSync, existsSync } from "fs";
6
+ import { statSync, readFileSync, existsSync, readdirSync, lstatSync } from "fs";
7
7
  import micromatch from "micromatch";
8
8
  import { castArray } from "lodash-es";
9
9
  import chokidar from "chokidar";
@@ -78,12 +78,12 @@ export class FileManager<
78
78
  });
79
79
 
80
80
  /** Returns an array of all relative file paths indexed by the file manager. */
81
- get fileIds() {
81
+ get fileIds(): string[] {
82
82
  return Array.from(this.files.keys());
83
83
  }
84
84
 
85
85
  /** Returns an array of all file metadata objects indexed by the file manager. */
86
- get fileObjects() {
86
+ get fileObjects(): File[] {
87
87
  return Array.from(this.files.values());
88
88
  }
89
89
 
@@ -92,7 +92,7 @@ export class FileManager<
92
92
  * @param {string | string[]} patterns - The patterns to match against the file IDs
93
93
  * @returns {string[]} The file IDs that match the patterns
94
94
  */
95
- match(patterns: string | string[]) {
95
+ match(patterns: string | string[]): string[] {
96
96
  return micromatch(this.files.keys(), patterns);
97
97
  }
98
98
 
@@ -102,7 +102,7 @@ export class FileManager<
102
102
  * @param {string | string[]} patterns - The patterns to match against the file IDs
103
103
  * @returns {File[]} The file objects that match the patterns
104
104
  */
105
- matchFiles(patterns: string | string[]) {
105
+ matchFiles(patterns: string | string[]): (File | undefined)[] {
106
106
  const fileIds = this.match(Array.isArray(patterns) ? patterns : [patterns]);
107
107
  return fileIds.map((fileId) => this.files.get(fileId));
108
108
  }
@@ -110,7 +110,7 @@ export class FileManager<
110
110
  /**
111
111
  * Returns the directory IDs for all of the files in the project.
112
112
  */
113
- get directoryIds() {
113
+ get directoryIds(): string[] {
114
114
  return Array.from(
115
115
  new Set(
116
116
  this.files
@@ -122,7 +122,7 @@ export class FileManager<
122
122
  }
123
123
 
124
124
  /** Returns an array of unique file extensions found across all indexed files. */
125
- get uniqueExtensions() {
125
+ get uniqueExtensions(): string[] {
126
126
  return Array.from(
127
127
  new Set(
128
128
  this.files.values().map((file) => file.extension)
@@ -131,17 +131,17 @@ export class FileManager<
131
131
  }
132
132
 
133
133
  /** Whether the file manager has completed its initial scan. */
134
- get isStarted() {
134
+ get isStarted(): boolean {
135
135
  return !!this.state.get("started");
136
136
  }
137
137
 
138
138
  /** Whether the file manager is currently performing its initial scan. */
139
- get isStarting() {
139
+ get isStarting(): boolean {
140
140
  return !!this.state.get("starting");
141
141
  }
142
142
 
143
143
  /** Whether the file watcher is actively monitoring for changes. */
144
- get isWatching() {
144
+ get isWatching(): boolean {
145
145
  return !!this.state.get("watching");
146
146
  }
147
147
 
@@ -156,7 +156,7 @@ export class FileManager<
156
156
  * @param {string | string[]} [options.exclude] - The patterns to exclude from the scan
157
157
  * @returns {Promise<FileManager>} The file manager instance
158
158
  */
159
- async start(options: { exclude?: string | string[] } = {}) {
159
+ async start(options: { exclude?: string | string[] } = {}): Promise<this> {
160
160
  if (this.isStarted) {
161
161
  return this;
162
162
  }
@@ -302,7 +302,7 @@ export class FileManager<
302
302
  * @param {string | string[]} [options.exclude] - The patterns to exclude from the scan
303
303
  * @returns {Promise<FileManager>} The file manager instance
304
304
  */
305
- async scanFiles(options: { exclude?: string | string[] } = {}) {
305
+ async scanFiles(options: { exclude?: string | string[] } = {}): Promise<this> {
306
306
  const { cwd, git, fs } = this.container;
307
307
 
308
308
  const fileIds: string[] = [];
@@ -339,7 +339,28 @@ export class FileManager<
339
339
  }
340
340
  }
341
341
  }
342
+
343
+ // git ls-files doesn't traverse symlinked directories — walk them via fs
344
+ // to pick up their contents. fs.walk now follows symlinks natively.
345
+ try {
346
+ const topEntries = readdirSync(cwd, { withFileTypes: true });
347
+ for (const entry of topEntries) {
348
+ if (entry.isSymbolicLink()) {
349
+ const fullPath = pathJoin(cwd, entry.name);
350
+ try {
351
+ const target = statSync(fullPath);
352
+ if (target.isDirectory()) {
353
+ const walked = await fs.walkAsync(fullPath, { exclude });
354
+ for (const absFile of walked.files) {
355
+ fileIds.push(relative(cwd, absFile));
356
+ }
357
+ }
358
+ } catch {}
359
+ }
360
+ }
361
+ } catch {}
342
362
  } else {
363
+ // fs.walkAsync follows symlinks, so non-git repos get symlink support for free
343
364
  await fs.walkAsync(cwd).then(({ files } : { files: string[] }) => fileIds.push(...files));
344
365
  }
345
366
 
@@ -395,7 +416,7 @@ export class FileManager<
395
416
  * @param {string | string[]} [options.exclude] - The patterns to exclude from the watch
396
417
  * @returns {Promise<void>}
397
418
  */
398
- async watch(options: { paths?: string | string[]; exclude?: string | string[] } = {}) {
419
+ async watch(options: { paths?: string | string[]; exclude?: string | string[] } = {}): Promise<void> {
399
420
  const pathsToWatch = castArray(options.paths || this.directoryIds.map(id => this.container.paths.resolve(id)))
400
421
  .map(p => this.container.paths.resolve(p));
401
422
 
@@ -466,7 +487,7 @@ export class FileManager<
466
487
  this.watcher = watcher;
467
488
  }
468
489
 
469
- async stopWatching() {
490
+ async stopWatching(): Promise<void> {
470
491
  if (!this.isWatching) {
471
492
  return;
472
493
  }
@@ -479,7 +500,7 @@ export class FileManager<
479
500
  }
480
501
  }
481
502
 
482
- async updateFile(path: string) {
503
+ async updateFile(path: string): Promise<void> {
483
504
  const absolutePath = this.container.paths.resolve(path);
484
505
  const { name, ext, dir } = parse(absolutePath);
485
506
 
@@ -505,7 +526,7 @@ export class FileManager<
505
526
  }
506
527
  }
507
528
 
508
- async removeFile(path: string) {
529
+ async removeFile(path: string): Promise<void> {
509
530
  this.files.delete(path);
510
531
  }
511
532
  }
@@ -9,11 +9,14 @@ import {
9
9
  readFileSync,
10
10
  cpSync,
11
11
  renameSync,
12
+ lstatSync,
13
+ realpathSync,
12
14
 
13
15
  rmSync as nodeRmSync,
16
+ type Stats,
14
17
  } from "fs";
15
18
  import { join, resolve, dirname, relative } from "path";
16
- import { readFile, stat, unlink, mkdir, writeFile, appendFile, readdir, cp, rename, rm as nodeRm } from "fs/promises";
19
+ import { readFile, stat, lstat, realpath, unlink, mkdir, writeFile, appendFile, readdir, cp, rename, rm as nodeRm } from "fs/promises";
17
20
  import { native as rimraf } from 'rimraf'
18
21
 
19
22
  type WalkOptions = {
@@ -40,6 +43,16 @@ function matchesPattern(filePath: string, patterns: string[]): boolean {
40
43
  })
41
44
  }
42
45
 
46
+ /** Sync: check if a symlink target is a directory */
47
+ function lstatFollowIsDir(fullPath: string): boolean {
48
+ try { return statSync(fullPath).isDirectory() } catch { return false }
49
+ }
50
+
51
+ /** Async: check if a symlink target is a directory */
52
+ async function lstatFollowIsDirAsync(fullPath: string): Promise<boolean> {
53
+ try { return (await stat(fullPath)).isDirectory() } catch { return false }
54
+ }
55
+
43
56
  /**
44
57
  * The FS feature provides methods for interacting with the file system, relative to the
45
58
  * container's cwd.
@@ -133,7 +146,7 @@ export class FS extends Feature {
133
146
  * console.log(config.version)
134
147
  * ```
135
148
  */
136
- readJson(path: string) {
149
+ readJson(path: string): any {
137
150
  return JSON.parse(this.readFile(path) as string)
138
151
  }
139
152
 
@@ -141,7 +154,7 @@ export class FS extends Feature {
141
154
  * Read and parse a JSON file synchronously
142
155
  * @alias readJson
143
156
  */
144
- readJsonSync(path: string) {
157
+ readJsonSync(path: string): any {
145
158
  return this.readJson(path)
146
159
  }
147
160
 
@@ -158,7 +171,7 @@ export class FS extends Feature {
158
171
  * console.log(config.version)
159
172
  * ```
160
173
  */
161
- async readJsonAsync(path: string) {
174
+ async readJsonAsync(path: string): Promise<any> {
162
175
  const content = await this.readFileAsync(path)
163
176
  return JSON.parse(content as string)
164
177
  }
@@ -176,7 +189,7 @@ export class FS extends Feature {
176
189
  * console.log(entries) // ['index.ts', 'utils.ts', 'components']
177
190
  * ```
178
191
  */
179
- readdirSync(path: string) {
192
+ readdirSync(path: string): string[] {
180
193
  return readdirSync(this.container.paths.resolve(path))
181
194
  }
182
195
 
@@ -193,7 +206,7 @@ export class FS extends Feature {
193
206
  * console.log(entries) // ['index.ts', 'utils.ts', 'components']
194
207
  * ```
195
208
  */
196
- async readdir(path: string) {
209
+ async readdir(path: string): Promise<string[]> {
197
210
  return await readdir(this.container.paths.resolve(path))
198
211
  }
199
212
 
@@ -214,7 +227,7 @@ export class FS extends Feature {
214
227
  * fs.writeFile('data.bin', Buffer.from([1, 2, 3, 4]))
215
228
  * ```
216
229
  */
217
- writeFile(path: string, content: Buffer | string) {
230
+ writeFile(path: string, content: Buffer | string): void {
218
231
  writeFileSync(this.container.paths.resolve(path), content)
219
232
  }
220
233
 
@@ -249,7 +262,7 @@ export class FS extends Feature {
249
262
  * fs.writeJson('config.json', { version: '1.0.0', debug: false })
250
263
  * ```
251
264
  */
252
- writeJson(path: string, data: any, indent: number = 2) {
265
+ writeJson(path: string, data: any, indent: number = 2): void {
253
266
  this.writeFile(path, JSON.stringify(data, null, indent) + '\n')
254
267
  }
255
268
 
@@ -282,7 +295,7 @@ export class FS extends Feature {
282
295
  * fs.appendFile('log.txt', 'New line\n')
283
296
  * ```
284
297
  */
285
- appendFile(path: string, content: Buffer | string) {
298
+ appendFile(path: string, content: Buffer | string): void {
286
299
  appendFileSync(this.container.paths.resolve(path), content)
287
300
  }
288
301
 
@@ -320,7 +333,7 @@ export class FS extends Feature {
320
333
  * fs.ensureFile('logs/app.log', '', false)
321
334
  * ```
322
335
  */
323
- ensureFile(path: string, content: string, overwrite = false) {
336
+ ensureFile(path: string, content: string, overwrite = false): string {
324
337
  path = this.container.paths.resolve(path);
325
338
 
326
339
  if (this.exists(path) && !overwrite) {
@@ -347,7 +360,7 @@ export class FS extends Feature {
347
360
  * await fs.ensureFileAsync('config/settings.json', '{}', true)
348
361
  * ```
349
362
  */
350
- async ensureFileAsync(path: string, content: string, overwrite = false) {
363
+ async ensureFileAsync(path: string, content: string, overwrite = false): Promise<string> {
351
364
  path = this.container.paths.resolve(path);
352
365
 
353
366
  if (this.exists(path) && !overwrite) {
@@ -372,7 +385,7 @@ export class FS extends Feature {
372
385
  * fs.ensureFolder('logs/debug')
373
386
  * ```
374
387
  */
375
- ensureFolder(path: string) {
388
+ ensureFolder(path: string): string {
376
389
  mkdirSync(this.container.paths.resolve(path), { recursive: true });
377
390
  return this.container.paths.resolve(path);
378
391
  }
@@ -389,7 +402,7 @@ export class FS extends Feature {
389
402
  * await fs.ensureFolderAsync('logs/debug')
390
403
  * ```
391
404
  */
392
- async ensureFolderAsync(path: string) {
405
+ async ensureFolderAsync(path: string): Promise<string> {
393
406
  const resolved = this.container.paths.resolve(path);
394
407
  await mkdir(resolved, { recursive: true });
395
408
  return resolved;
@@ -406,7 +419,7 @@ export class FS extends Feature {
406
419
  * fs.mkdirp('deep/nested/path')
407
420
  * ```
408
421
  */
409
- mkdirp(folder: string) {
422
+ mkdirp(folder: string): string {
410
423
  return this.ensureFolder(folder)
411
424
  }
412
425
 
@@ -467,11 +480,33 @@ export class FS extends Feature {
467
480
  * }
468
481
  * ```
469
482
  */
470
- async existsAsync(path: string) {
483
+ async existsAsync(path: string): Promise<boolean> {
471
484
  const filePath = this.container.paths.resolve(path);
472
485
  return stat(filePath).then(() => true).catch(() => false)
473
486
  }
474
487
 
488
+ /**
489
+ * Checks if a path is a symbolic link.
490
+ *
491
+ * @param {string} path - The path to check
492
+ * @returns {boolean} True if the path is a symlink
493
+ */
494
+ isSymlink(path: string): boolean {
495
+ const filePath = this.container.paths.resolve(path);
496
+ try { return lstatSync(filePath).isSymbolicLink() } catch { return false }
497
+ }
498
+
499
+ /**
500
+ * Resolves a symlink to its real path. Returns the resolved path as-is if not a symlink.
501
+ *
502
+ * @param {string} path - The path to resolve
503
+ * @returns {string} The real path after resolving all symlinks
504
+ */
505
+ realpath(path: string): string {
506
+ const filePath = this.container.paths.resolve(path);
507
+ return realpathSync(filePath)
508
+ }
509
+
475
510
  /**
476
511
  * Synchronously returns the stat object for a file or directory.
477
512
  *
@@ -485,7 +520,7 @@ export class FS extends Feature {
485
520
  * console.log(info.size, info.mtime)
486
521
  * ```
487
522
  */
488
- stat(path: string) {
523
+ stat(path: string): Stats {
489
524
  return statSync(this.container.paths.resolve(path))
490
525
  }
491
526
 
@@ -502,7 +537,7 @@ export class FS extends Feature {
502
537
  * console.log(info.size, info.mtime)
503
538
  * ```
504
539
  */
505
- async statAsync(path: string) {
540
+ async statAsync(path: string): Promise<Stats> {
506
541
  return stat(this.container.paths.resolve(path))
507
542
  }
508
543
 
@@ -597,7 +632,7 @@ export class FS extends Feature {
597
632
  * fs.rmSync('temp/cache.tmp')
598
633
  * ```
599
634
  */
600
- rmSync(path: string) {
635
+ rmSync(path: string): void {
601
636
  nodeRmSync(this.container.paths.resolve(path), { force: true })
602
637
  }
603
638
 
@@ -628,7 +663,7 @@ export class FS extends Feature {
628
663
  * fs.rmdirSync('temp/cache')
629
664
  * ```
630
665
  */
631
- rmdirSync(dirPath: string) {
666
+ rmdirSync(dirPath: string): void {
632
667
  nodeRmSync(this.container.paths.resolve(dirPath), { recursive: true, force: true })
633
668
  }
634
669
 
@@ -668,7 +703,7 @@ export class FS extends Feature {
668
703
  * fs.copy('src', 'backup/src')
669
704
  * ```
670
705
  */
671
- copy(src: string, dest: string, options: { overwrite?: boolean } = {}) {
706
+ copy(src: string, dest: string, options: { overwrite?: boolean } = {}): void {
672
707
  const { overwrite = true } = options
673
708
  const resolvedSrc = this.container.paths.resolve(src)
674
709
  const resolvedDest = this.container.paths.resolve(dest)
@@ -712,7 +747,7 @@ export class FS extends Feature {
712
747
  * fs.move('old-dir', 'new-dir')
713
748
  * ```
714
749
  */
715
- move(src: string, dest: string) {
750
+ move(src: string, dest: string): void {
716
751
  const resolvedSrc = this.container.paths.resolve(src)
717
752
  const resolvedDest = this.container.paths.resolve(dest)
718
753
  const destDir = dirname(resolvedDest)
@@ -785,7 +820,7 @@ export class FS extends Feature {
785
820
  * const relative = fs.walk('inbox', { relative: true }) // => { files: ['contact-1.json', ...] }
786
821
  * ```
787
822
  */
788
- walk(basePath: string, options: WalkOptions = {}) {
823
+ walk(basePath: string, options: WalkOptions = {}): { directories: string[], files: string[] } {
789
824
  const {
790
825
  directories = true,
791
826
  files = true,
@@ -811,7 +846,9 @@ export class FS extends Feature {
811
846
  const fullPath = join(baseDir, name);
812
847
  const relativePath = relative(resolvedBase, fullPath)
813
848
  const outputPath = useRelative ? relativePath : fullPath;
814
- const isDir = entry.isDirectory();
849
+ // Follow symlinks: isDirectory() returns false for symlinks,
850
+ // so check isSymbolicLink() and resolve to the real target
851
+ const isDir = entry.isDirectory() || (entry.isSymbolicLink() && lstatFollowIsDir(fullPath));
815
852
 
816
853
  if (excludePatterns.length && matchesPattern(relativePath, excludePatterns)) {
817
854
  continue
@@ -862,7 +899,7 @@ export class FS extends Feature {
862
899
  * // files.files => ['contact-1.json', 'subfolder/file.txt', ...]
863
900
  * ```
864
901
  */
865
- async walkAsync(baseDir: string, options: WalkOptions = {}) {
902
+ async walkAsync(baseDir: string, options: WalkOptions = {}): Promise<{ directories: string[], files: string[] }> {
866
903
  const {
867
904
  directories = true,
868
905
  files = true,
@@ -888,7 +925,9 @@ export class FS extends Feature {
888
925
  const fullPath = join(currentDir, name);
889
926
  const relativePath = relative(resolvedBase, fullPath)
890
927
  const outputPath = useRelative ? relativePath : fullPath;
891
- const isDir = entry.isDirectory();
928
+ // Follow symlinks: isDirectory() returns false for symlinks,
929
+ // so check isSymbolicLink() and resolve to the real target
930
+ const isDir = entry.isDirectory() || (entry.isSymbolicLink() && await lstatFollowIsDirAsync(fullPath));
892
931
 
893
932
  if (excludePatterns.length && matchesPattern(relativePath, excludePatterns)) {
894
933
  continue
@@ -98,7 +98,7 @@ export class Git extends Feature {
98
98
  * })
99
99
  * ```
100
100
  */
101
- async lsFiles(options: LsFilesOptions = {}) {
101
+ async lsFiles(options: LsFilesOptions = {}): Promise<string[]> {
102
102
  const {
103
103
  cached = false,
104
104
  deleted = false,
@@ -153,7 +153,7 @@ export class Git extends Feature {
153
153
  * }
154
154
  * ```
155
155
  */
156
- get branch() {
156
+ get branch(): string | null {
157
157
  if(!this.isRepo) { return null }
158
158
  return this.container.feature('proc').exec(`${this.gitPath} branch`).split("\n").filter(line => line.startsWith('*')).map(line => line.replace('*', '').trim()).pop()
159
159
  }
@@ -171,7 +171,7 @@ export class Git extends Feature {
171
171
  * }
172
172
  * ```
173
173
  */
174
- get sha() {
174
+ get sha(): string | null {
175
175
  if(!this.isRepo) { return null }
176
176
  return this.container.feature('proc').exec(`${this.gitPath} rev-parse HEAD`, { cwd: this.repoRoot })
177
177
  }
@@ -190,7 +190,7 @@ export class Git extends Feature {
190
190
  * }
191
191
  * ```
192
192
  */
193
- get isRepo() {
193
+ get isRepo(): boolean {
194
194
  return !!this.repoRoot
195
195
  }
196
196
 
@@ -208,7 +208,7 @@ export class Git extends Feature {
208
208
  * }
209
209
  * ```
210
210
  */
211
- get isRepoRoot() {
211
+ get isRepoRoot(): boolean {
212
212
  return this.repoRoot == this.container.cwd
213
213
  }
214
214
 
@@ -228,7 +228,7 @@ export class Git extends Feature {
228
228
  * }
229
229
  * ```
230
230
  */
231
- get repoRoot() {
231
+ get repoRoot(): string | null {
232
232
  if (this.state.has('repoRoot')) {
233
233
  return this.state.get('repoRoot')
234
234
  }
@@ -260,7 +260,7 @@ export class Git extends Feature {
260
260
  * }
261
261
  * ```
262
262
  */
263
- async getLatestChanges(numberOfChanges: number = 10) {
263
+ async getLatestChanges(numberOfChanges: number = 10): Promise<Array<{ title: string, message: string, author: string }>> {
264
264
  if (!this.isRepo) return []
265
265
 
266
266
  const separator = '---COMMIT---'
@@ -299,7 +299,7 @@ export class Git extends Feature {
299
299
  * }
300
300
  * ```
301
301
  */
302
- fileLog(...files: string[]) {
302
+ fileLog(...files: string[]): Array<{ sha: string, message: string }> {
303
303
  if (!this.isRepo || !files.length) return []
304
304
 
305
305
  const proc = this.container.feature('proc')
@@ -349,7 +349,7 @@ export class Git extends Feature {
349
349
  * const d = git.diff('src/index.ts', 'feature-branch', 'main')
350
350
  * ```
351
351
  */
352
- diff(file: string, compareTo: string, compareFrom?: string) {
352
+ diff(file: string, compareTo: string, compareFrom?: string): string {
353
353
  if (!this.isRepo) return ''
354
354
 
355
355
  const proc = this.container.feature('proc')
@@ -531,7 +531,7 @@ export class Git extends Feature {
531
531
  * const history = git.getChangeHistoryForFiles('src/node/features/*.ts')
532
532
  * ```
533
533
  */
534
- getChangeHistoryForFiles(...paths: string[]) {
534
+ getChangeHistoryForFiles(...paths: string[]): Array<{ sha: string, message: string, longMessage: string, filesMatched: string[] }> {
535
535
  if (!this.isRepo || !paths.length) return []
536
536
 
537
537
  const proc = this.container.feature('proc')
@@ -438,24 +438,22 @@ export class Helpers extends Feature<HelpersState, HelpersOptions> {
438
438
  }
439
439
  } catch {}
440
440
 
441
- // Try fileManager first (faster in git repos), fall back to Glob
441
+ // Try fileManager first (faster in git repos, and now symlink-aware),
442
+ // fall back to fs.walk which also follows symlinks natively.
442
443
  let files: string[] = []
443
444
  try {
444
445
  const fm = await this.ensureFileManager()
445
- // fileManager may store absolute or relative keys — use absolute patterns
446
446
  const absPatterns = [`${dir}/*.ts`, `${dir}/**/*.ts`]
447
447
  const relPatterns = [`${type}/*.ts`, `${type}/**/*.ts`]
448
448
  const matched = fm.match([...absPatterns, ...relPatterns])
449
449
  files = matched.map((f: string) => f.startsWith('/') ? f : resolve(this.rootDir, f))
450
450
  } catch {}
451
451
 
452
- // Fall back to Glob if fileManager found nothing
452
+ // Fall back to fs.walk if fileManager found nothing
453
453
  if (files.length === 0) {
454
- const { Glob } = globalThis.Bun || (await import('bun'))
455
- const glob = new Glob('**/*.ts')
456
- for await (const file of glob.scan({ cwd: dir })) {
457
- files.push(resolve(dir, file))
458
- }
454
+ const { fs } = this.container
455
+ const walked = fs.walk(dir, { include: ['**/*.ts'] })
456
+ files = walked.files
459
457
  }
460
458
 
461
459
  for (const absPath of files) {
@@ -550,10 +548,15 @@ export class Helpers extends Feature<HelpersState, HelpersOptions> {
550
548
  */
551
549
  private async discoverCommandsViaVM(dir: string): Promise<void> {
552
550
  this.seedVirtualModules()
553
- const { Glob } = globalThis.Bun || (await import('bun'))
554
- const glob = new Glob('*.ts')
555
-
556
- for await (const file of glob.scan({ cwd: dir })) {
551
+ const { fs } = this.container
552
+ // Commands are top-level only (not recursive) — walk and filter to *.ts in the immediate dir
553
+ const walked = fs.walk(dir, { include: ['*.ts'] })
554
+ const tsFiles = walked.files
555
+ .map(f => parse(f))
556
+ .filter(p => p.dir === dir) // top-level only
557
+ .map(p => p.base)
558
+
559
+ for (const file of tsFiles) {
557
560
  if (file === 'index.ts') continue
558
561
 
559
562
  const absPath = resolve(dir, file)
@@ -619,10 +622,14 @@ export class Helpers extends Feature<HelpersState, HelpersOptions> {
619
622
  */
620
623
  private async discoverSelectorsViaVM(dir: string): Promise<void> {
621
624
  this.seedVirtualModules()
622
- const { Glob } = globalThis.Bun || (await import('bun'))
623
- const glob = new Glob('*.ts')
625
+ const { fs } = this.container
626
+ const walked = fs.walk(dir, { include: ['*.ts'] })
627
+ const tsFiles = walked.files
628
+ .map(f => parse(f))
629
+ .filter(p => p.dir === dir)
630
+ .map(p => p.base)
624
631
 
625
- for await (const file of glob.scan({ cwd: dir })) {
632
+ for (const file of tsFiles) {
626
633
  if (file === 'index.ts') continue
627
634
 
628
635
  const absPath = resolve(dir, file)
@@ -661,10 +668,10 @@ export class Helpers extends Feature<HelpersState, HelpersOptions> {
661
668
  * Actual mounting to an express server is handled separately by ExpressServer.useEndpoints().
662
669
  */
663
670
  private async discoverEndpoints(dir: string): Promise<void> {
664
- const { Glob } = globalThis.Bun || (await import('bun'))
665
- const glob = new Glob('**/*.ts')
671
+ const { fs } = this.container
672
+ const walked = fs.walk(dir, { include: ['**/*.ts'] })
666
673
 
667
- for await (const file of glob.scan({ cwd: dir, absolute: true })) {
674
+ for (const file of walked.files) {
668
675
  try {
669
676
  const mod = await this.loadModuleExports(file)
670
677
  const endpointModule = mod.default || mod