@neuralis/brain-core 0.3.1 → 0.4.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 +88 -1417
- package/app/README.md +4 -4
- package/app/cards/FilesystemCardShell.tsx +42 -4
- package/app/cards/FilesystemChangeCard.tsx +34 -11
- package/app/cards/README.md +6 -5
- package/app/cards/diffUtils.ts +99 -1
- package/app/filesystem/FilesystemHost.tsx +24 -2
- package/app/filesystem/README.md +1 -1
- package/app/filesystem/api/README.md +3 -3
- package/app/filesystem/api/index.ts +38 -14
- package/app/filesystem/api/projectPackages.ts +13 -102
- package/app/filesystem/domain/types.ts +30 -10
- package/app/filesystem/hooks/README.md +4 -4
- package/app/filesystem/hooks/useDiscoverableSources.ts +19 -30
- package/app/filesystem/hooks/useFilesystemSSE.ts +42 -2
- package/app/filesystem/host/README.md +6 -4
- package/app/filesystem/host/buildFilesystemHostValue.ts +28 -2
- package/app/filesystem/host/installFilesystemHostComponents.tsx +1 -1
- package/app/filesystem/lib/sourceKindForm.ts +210 -27
- package/app/filesystem/lib/treeVisualState.tsx +98 -4
- package/app/filesystem/store/README.md +1 -0
- package/app/filesystem/store/filesystemStore.ts +99 -8
- package/app/filesystem/store/gitSlice.ts +5 -0
- package/app/filesystem/store/helpers.ts +17 -0
- package/app/filesystem/store/listSlice.ts +8 -0
- package/app/filesystem/ui/CodeEditor.tsx +18 -3
- package/app/filesystem/ui/DiscoverDock.tsx +5 -1
- package/app/filesystem/ui/FileEditor.tsx +15 -10
- package/app/filesystem/ui/FileTree.tsx +75 -24
- package/app/filesystem/ui/FilesystemWidget.tsx +10 -2
- package/app/filesystem/ui/FolderPicker.tsx +251 -0
- package/app/filesystem/ui/GitControlPanel.tsx +7 -2
- package/app/filesystem/ui/PackagesPanel.tsx +52 -374
- package/app/filesystem/ui/PendingChangesPanel.tsx +1 -1
- package/app/filesystem/ui/PolicyEditor.tsx +109 -24
- package/app/filesystem/ui/README.md +30 -23
- package/app/filesystem/ui/RawMediaPreview.tsx +58 -0
- package/app/filesystem/ui/SourceBranchHeader.tsx +4 -4
- package/app/filesystem/ui/SourcesPanel.tsx +502 -342
- package/app/filesystem/ui/Toolbar.tsx +1 -2
- package/app/filesystem/ui/VectorHealthPanel.tsx +2 -2
- package/app/shared/filesystem.png +0 -0
- package/dist/app/host.js +58 -46
- package/dist/scripts/sourceRootPairV1.d.ts +47 -0
- package/dist/scripts/sourceRootPairV1.d.ts.map +1 -0
- package/dist/scripts/sourceRootPairV1.js +147 -0
- package/dist/scripts/sourceRootPairV1.js.map +1 -0
- package/dist/scripts/sourceToFolderV1.d.ts +56 -0
- package/dist/scripts/sourceToFolderV1.d.ts.map +1 -0
- package/dist/scripts/sourceToFolderV1.js +152 -0
- package/dist/scripts/sourceToFolderV1.js.map +1 -0
- package/dist/scripts/sourceToHostV1.d.ts +56 -0
- package/dist/scripts/sourceToHostV1.d.ts.map +1 -0
- package/dist/scripts/sourceToHostV1.js +132 -0
- package/dist/scripts/sourceToHostV1.js.map +1 -0
- package/dist/src/channels/filesystem.d.ts +1 -1
- package/dist/src/channels/filesystem.js +1 -1
- package/dist/src/connectors/AppSourceConfigStore.d.ts +17 -3
- package/dist/src/connectors/AppSourceConfigStore.d.ts.map +1 -1
- package/dist/src/connectors/AppSourceConfigStore.js +46 -6
- package/dist/src/connectors/AppSourceConfigStore.js.map +1 -1
- package/dist/src/connectors/HostConnector.d.ts +201 -0
- package/dist/src/connectors/HostConnector.d.ts.map +1 -0
- package/dist/src/connectors/HostConnector.js +903 -0
- package/dist/src/connectors/HostConnector.js.map +1 -0
- package/dist/src/connectors/inProcessTransport.d.ts +131 -0
- package/dist/src/connectors/inProcessTransport.d.ts.map +1 -0
- package/dist/src/connectors/inProcessTransport.js +447 -0
- package/dist/src/connectors/inProcessTransport.js.map +1 -0
- package/dist/src/domain/access/callerPermissions.d.ts +29 -0
- package/dist/src/domain/access/callerPermissions.d.ts.map +1 -1
- package/dist/src/domain/access/callerPermissions.js +85 -5
- package/dist/src/domain/access/callerPermissions.js.map +1 -1
- package/dist/src/domain/access/nativeLocalFence.d.ts +65 -0
- package/dist/src/domain/access/nativeLocalFence.d.ts.map +1 -0
- package/dist/src/domain/access/nativeLocalFence.js +171 -0
- package/dist/src/domain/access/nativeLocalFence.js.map +1 -0
- package/dist/src/domain/access/uriPermission.js +3 -3
- package/dist/src/domain/access/uriPermission.js.map +1 -1
- package/dist/src/domain/byteRange.d.ts +15 -0
- package/dist/src/domain/byteRange.d.ts.map +1 -0
- package/dist/src/domain/byteRange.js +30 -0
- package/dist/src/domain/byteRange.js.map +1 -0
- package/dist/src/domain/defaultPermissions.d.ts.map +1 -1
- package/dist/src/domain/defaultPermissions.js +29 -0
- package/dist/src/domain/defaultPermissions.js.map +1 -1
- package/dist/src/domain/mime.d.ts +11 -19
- package/dist/src/domain/mime.d.ts.map +1 -1
- package/dist/src/domain/mime.js +13 -102
- package/dist/src/domain/mime.js.map +1 -1
- package/dist/src/domain/paths.d.ts.map +1 -1
- package/dist/src/domain/paths.js +5 -3
- package/dist/src/domain/paths.js.map +1 -1
- package/dist/src/domain/scopeAccessCtx.d.ts +2 -2
- package/dist/src/domain/scopeAccessCtx.js +2 -2
- package/dist/src/domain/sourceConfig.d.ts +52 -19
- package/dist/src/domain/sourceConfig.d.ts.map +1 -1
- package/dist/src/domain/sourceConfig.js +103 -19
- package/dist/src/domain/sourceConfig.js.map +1 -1
- package/dist/src/domain/sourcePackages.d.ts +8 -8
- package/dist/src/domain/sourcePackages.d.ts.map +1 -1
- package/dist/src/domain/sourceRootPair.d.ts +88 -0
- package/dist/src/domain/sourceRootPair.d.ts.map +1 -0
- package/dist/src/domain/sourceRootPair.js +108 -0
- package/dist/src/domain/sourceRootPair.js.map +1 -0
- package/dist/src/domain/syncTypes.d.ts +8 -1
- package/dist/src/domain/syncTypes.d.ts.map +1 -1
- package/dist/src/domain/syncTypes.js +1 -0
- package/dist/src/domain/syncTypes.js.map +1 -1
- package/dist/src/domain/types.d.ts +32 -2
- package/dist/src/domain/types.d.ts.map +1 -1
- package/dist/src/domain/uri.d.ts.map +1 -1
- package/dist/src/domain/uri.js +3 -2
- package/dist/src/domain/uri.js.map +1 -1
- package/dist/src/embeddings/clients/GeminiEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/GeminiEmbedder.js +2 -5
- package/dist/src/embeddings/clients/GeminiEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/OpenAIEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/OpenAIEmbedder.js +2 -3
- package/dist/src/embeddings/clients/OpenAIEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/QwenEmbedder.d.ts +2 -0
- package/dist/src/embeddings/clients/QwenEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/QwenEmbedder.js +10 -2
- package/dist/src/embeddings/clients/QwenEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/VoyageEmbedder.d.ts +2 -0
- package/dist/src/embeddings/clients/VoyageEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/VoyageEmbedder.js +8 -1
- package/dist/src/embeddings/clients/VoyageEmbedder.js.map +1 -1
- package/dist/src/embeddings/factory.d.ts +5 -3
- package/dist/src/embeddings/factory.d.ts.map +1 -1
- package/dist/src/embeddings/factory.js +7 -3
- package/dist/src/embeddings/factory.js.map +1 -1
- package/dist/src/embeddings/models.d.ts +1 -1
- package/dist/src/embeddings/models.d.ts.map +1 -1
- package/dist/src/embeddings/retry.js +1 -1
- package/dist/src/embeddings/retry.js.map +1 -1
- package/dist/src/embeddings/types.d.ts +6 -0
- package/dist/src/embeddings/types.d.ts.map +1 -1
- package/dist/src/embeddings/types.js +11 -0
- package/dist/src/embeddings/types.js.map +1 -1
- package/dist/src/events/pending.d.ts +1 -1
- package/dist/src/events/pending.js +1 -1
- package/dist/src/git/GitRunner.d.ts +110 -30
- package/dist/src/git/GitRunner.d.ts.map +1 -1
- package/dist/src/git/GitRunner.js +511 -199
- package/dist/src/git/GitRunner.js.map +1 -1
- package/dist/src/git/GitWatchRegistry.d.ts +41 -35
- package/dist/src/git/GitWatchRegistry.d.ts.map +1 -1
- package/dist/src/git/GitWatchRegistry.js +61 -79
- package/dist/src/git/GitWatchRegistry.js.map +1 -1
- package/dist/src/git/gitPlace.d.ts +61 -0
- package/dist/src/git/gitPlace.d.ts.map +1 -0
- package/dist/src/git/gitPlace.js +74 -0
- package/dist/src/git/gitPlace.js.map +1 -0
- package/dist/src/git/sourceGitControl.d.ts +33 -21
- package/dist/src/git/sourceGitControl.d.ts.map +1 -1
- package/dist/src/git/sourceGitControl.js +211 -95
- package/dist/src/git/sourceGitControl.js.map +1 -1
- package/dist/src/git/sourceGitStatus.js +1 -1
- package/dist/src/git/sourceGitStatus.js.map +1 -1
- package/dist/src/git/untrackedStats.js +1 -1
- package/dist/src/git/untrackedStats.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lifecycle.d.ts +9 -12
- package/dist/src/lifecycle.d.ts.map +1 -1
- package/dist/src/lifecycle.js +2 -1
- package/dist/src/lifecycle.js.map +1 -1
- package/dist/src/routes/approve.d.ts +1 -1
- package/dist/src/routes/approve.js +1 -1
- package/dist/src/routes/events.d.ts +1 -1
- package/dist/src/routes/events.js +1 -1
- package/dist/src/routes/git.d.ts +7 -7
- package/dist/src/routes/git.d.ts.map +1 -1
- package/dist/src/routes/git.js +201 -72
- package/dist/src/routes/git.js.map +1 -1
- package/dist/src/routes/health.d.ts +6 -6
- package/dist/src/routes/health.js +12 -12
- package/dist/src/routes/helpers/sourceNotConfigured.d.ts +1 -1
- package/dist/src/routes/helpers/sourceNotConfigured.js +1 -1
- package/dist/src/routes/helpers/uriGuard.d.ts +1 -1
- package/dist/src/routes/helpers/uriGuard.d.ts.map +1 -1
- package/dist/src/routes/helpers/uriGuard.js +3 -2
- package/dist/src/routes/helpers/uriGuard.js.map +1 -1
- package/dist/src/routes/helpers.d.ts +11 -4
- package/dist/src/routes/helpers.d.ts.map +1 -1
- package/dist/src/routes/helpers.js +13 -6
- package/dist/src/routes/helpers.js.map +1 -1
- package/dist/src/routes/list.d.ts +1 -1
- package/dist/src/routes/list.d.ts.map +1 -1
- package/dist/src/routes/list.js +3 -4
- package/dist/src/routes/list.js.map +1 -1
- package/dist/src/routes/pending.d.ts +4 -4
- package/dist/src/routes/pending.js +9 -9
- package/dist/src/routes/policies.d.ts +10 -8
- package/dist/src/routes/policies.d.ts.map +1 -1
- package/dist/src/routes/policies.js +31 -25
- package/dist/src/routes/policies.js.map +1 -1
- package/dist/src/routes/raw.d.ts +1 -1
- package/dist/src/routes/raw.d.ts.map +1 -1
- package/dist/src/routes/raw.js +66 -12
- package/dist/src/routes/raw.js.map +1 -1
- package/dist/src/routes/read.d.ts +1 -1
- package/dist/src/routes/read.js +1 -1
- package/dist/src/routes/search.d.ts +1 -1
- package/dist/src/routes/search.js +1 -1
- package/dist/src/routes/sources.d.ts +63 -6
- package/dist/src/routes/sources.d.ts.map +1 -1
- package/dist/src/routes/sources.js +494 -382
- package/dist/src/routes/sources.js.map +1 -1
- package/dist/src/routes/upload.d.ts +1 -1
- package/dist/src/routes/upload.js +1 -1
- package/dist/src/routes/write.d.ts +2 -2
- package/dist/src/routes/write.js +2 -2
- package/dist/src/services/BridgeService.d.ts +21 -35
- package/dist/src/services/BridgeService.d.ts.map +1 -1
- package/dist/src/services/BridgeService.js +42 -88
- package/dist/src/services/BridgeService.js.map +1 -1
- package/dist/src/services/FilesystemService.d.ts +4 -3
- package/dist/src/services/FilesystemService.d.ts.map +1 -1
- package/dist/src/services/FilesystemService.js +112 -34
- package/dist/src/services/FilesystemService.js.map +1 -1
- package/dist/src/services/FilesystemUiService.d.ts +4 -1
- package/dist/src/services/FilesystemUiService.d.ts.map +1 -1
- package/dist/src/services/FilesystemUiService.js +66 -25
- package/dist/src/services/FilesystemUiService.js.map +1 -1
- package/dist/src/services/SyncScheduler.d.ts +12 -4
- package/dist/src/services/SyncScheduler.d.ts.map +1 -1
- package/dist/src/services/SyncScheduler.js +37 -8
- package/dist/src/services/SyncScheduler.js.map +1 -1
- package/dist/src/services/baselineRepair.d.ts +13 -3
- package/dist/src/services/baselineRepair.d.ts.map +1 -1
- package/dist/src/services/baselineRepair.js +13 -3
- package/dist/src/services/baselineRepair.js.map +1 -1
- package/dist/src/services/declaredSources.d.ts +132 -43
- package/dist/src/services/declaredSources.d.ts.map +1 -1
- package/dist/src/services/declaredSources.js +118 -45
- package/dist/src/services/declaredSources.js.map +1 -1
- package/dist/src/store/dataFormats.d.ts +17 -2
- package/dist/src/store/dataFormats.d.ts.map +1 -1
- package/dist/src/store/dataFormats.js +18 -3
- package/dist/src/store/dataFormats.js.map +1 -1
- package/dist/src/sync/DeltaSyncRunner.d.ts.map +1 -1
- package/dist/src/sync/DeltaSyncRunner.js +144 -4
- package/dist/src/sync/DeltaSyncRunner.js.map +1 -1
- package/dist/src/sync/SyncJobQueue.d.ts +7 -2
- package/dist/src/sync/SyncJobQueue.d.ts.map +1 -1
- package/dist/src/sync/SyncJobQueue.js +16 -10
- package/dist/src/sync/SyncJobQueue.js.map +1 -1
- package/dist/src/sync/SyncWatchService.d.ts +21 -4
- package/dist/src/sync/SyncWatchService.d.ts.map +1 -1
- package/dist/src/sync/SyncWatchService.js +115 -10
- package/dist/src/sync/SyncWatchService.js.map +1 -1
- package/dist/src/sync/categoryInference.d.ts +1 -1
- package/dist/src/sync/categoryInference.js +1 -1
- package/dist/src/tools/execCtx.d.ts +2 -2
- package/dist/src/tools/execCtx.js +2 -2
- package/dist/src/tools/fs_read.d.ts.map +1 -1
- package/dist/src/tools/fs_read.js +5 -0
- package/dist/src/tools/fs_read.js.map +1 -1
- package/dist/src/usecases/admitSourceRoot.d.ts +65 -0
- package/dist/src/usecases/admitSourceRoot.d.ts.map +1 -0
- package/dist/src/usecases/admitSourceRoot.js +85 -0
- package/dist/src/usecases/admitSourceRoot.js.map +1 -0
- package/dist/src/usecases/browseSourceFolders.d.ts +85 -0
- package/dist/src/usecases/browseSourceFolders.d.ts.map +1 -0
- package/dist/src/usecases/browseSourceFolders.js +210 -0
- package/dist/src/usecases/browseSourceFolders.js.map +1 -0
- package/dist/src/usecases/createDeclaredSources.d.ts +13 -2
- package/dist/src/usecases/createDeclaredSources.d.ts.map +1 -1
- package/dist/src/usecases/createDeclaredSources.js +39 -0
- package/dist/src/usecases/createDeclaredSources.js.map +1 -1
- package/dist/src/usecases/embeddingBacklog.js +1 -1
- package/dist/src/usecases/embeddingBacklog.js.map +1 -1
- package/dist/src/usecases/list.d.ts.map +1 -1
- package/dist/src/usecases/list.js +83 -26
- package/dist/src/usecases/list.js.map +1 -1
- package/dist/src/usecases/listAttachableDevices.d.ts +13 -0
- package/dist/src/usecases/listAttachableDevices.d.ts.map +1 -0
- package/dist/src/usecases/listAttachableDevices.js +10 -0
- package/dist/src/usecases/listAttachableDevices.js.map +1 -0
- package/dist/src/usecases/listSourcePackages.d.ts.map +1 -1
- package/dist/src/usecases/listSourcePackages.js +2 -5
- package/dist/src/usecases/listSourcePackages.js.map +1 -1
- package/dist/src/usecases/purgeExcludedPoints.d.ts.map +1 -1
- package/dist/src/usecases/purgeExcludedPoints.js +3 -2
- package/dist/src/usecases/purgeExcludedPoints.js.map +1 -1
- package/dist/src/usecases/readFiles.d.ts.map +1 -1
- package/dist/src/usecases/readFiles.js +99 -1
- package/dist/src/usecases/readFiles.js.map +1 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.d.ts +1 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.d.ts.map +1 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.js +2 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.js.map +1 -1
- package/dist/src/usecases/revokePrincipal.d.ts +7 -5
- package/dist/src/usecases/revokePrincipal.d.ts.map +1 -1
- package/dist/src/usecases/revokePrincipal.js +29 -7
- package/dist/src/usecases/revokePrincipal.js.map +1 -1
- package/dist/src/usecases/scheduleSourceSync.d.ts +7 -0
- package/dist/src/usecases/scheduleSourceSync.d.ts.map +1 -1
- package/dist/src/usecases/scheduleSourceSync.js +6 -5
- package/dist/src/usecases/scheduleSourceSync.js.map +1 -1
- package/dist/src/usecases/syncPointWrites.d.ts +2 -0
- package/dist/src/usecases/syncPointWrites.d.ts.map +1 -1
- package/dist/src/usecases/syncPointWrites.js +5 -1
- package/dist/src/usecases/syncPointWrites.js.map +1 -1
- package/dist/src/usecases/triggerSourceSync.d.ts +46 -0
- package/dist/src/usecases/triggerSourceSync.d.ts.map +1 -0
- package/dist/src/usecases/triggerSourceSync.js +75 -0
- package/dist/src/usecases/triggerSourceSync.js.map +1 -0
- package/dist/src/usecases/uploadBinary.d.ts +2 -1
- package/dist/src/usecases/uploadBinary.d.ts.map +1 -1
- package/dist/src/usecases/uploadBinary.js +2 -2
- package/dist/src/usecases/uploadBinary.js.map +1 -1
- package/dist/src/usecases/vector/endpointGuard.d.ts +1 -1
- package/dist/src/usecases/vector/endpointGuard.js +1 -1
- package/dist/src/usecases/vectorMaintenance.js +1 -1
- package/dist/src/usecases/vectorMaintenance.js.map +1 -1
- package/dist/src/usecases/write/content.js +1 -1
- package/dist/src/vector/qdrantClient.d.ts +1 -1
- package/dist/src/vector/qdrantClient.js +1 -1
- package/dist/src/wiring.d.ts +3 -0
- package/dist/src/wiring.d.ts.map +1 -1
- package/dist/src/wiring.js +72 -6
- package/dist/src/wiring.js.map +1 -1
- package/dist/tools/fs_list.json +1 -1
- package/dist/tools/fs_read.json +1 -1
- package/dist/tools/fs_search.json +1 -1
- package/dist/tools/fs_write.json +1 -1
- package/instructions/brain-model.md +1 -1
- package/package.json +212 -114
- package/rules/brain-rules.md +2 -2
- package/skills/brain-start/SKILL.md +1 -1
- package/skills/connect-business-data/SKILL.md +3 -3
- package/skills/create-package/SKILL.md +26 -26
- package/skills/create-package/references/config-and-credentials.md +16 -13
- package/skills/create-package/references/connectors-and-uri-policies.md +39 -25
- package/skills/create-package/references/manifest-and-discovery.md +12 -12
- package/skills/create-package/references/runtime-and-build.md +19 -19
- package/skills/create-package/references/skills-and-context.md +36 -20
- package/skills/create-package/references/surfaces-and-design.md +16 -16
- package/skills/create-package/references/team-and-workflows.md +2 -2
- package/skills/create-package/references/test-and-ship.md +21 -21
- package/skills/create-package/references/tools-and-schemas.md +5 -5
- package/skills/create-package/scripts/{check-package.sh → check-package.mjs} +79 -65
- package/skills/create-package/scripts/{verify-loaded.sh → verify-loaded.mjs} +156 -162
- package/skills/git/SKILL.md +53 -51
- package/skills/manage-files/SKILL.md +22 -25
- package/skills/manage-sources/SKILL.md +128 -87
- package/skills/manage-uri-policy/SKILL.md +18 -15
- package/skills/organize/SKILL.md +1 -1
- package/skills/sync/SKILL.md +3 -3
- package/tools/fs_list.json +1 -1
- package/tools/fs_read.json +1 -1
- package/tools/fs_search.json +1 -1
- package/tools/fs_write.json +1 -1
- package/workflows/sources-digest.json +1 -1
- package/dist/src/connectors/LocalDiskConnector.d.ts +0 -171
- package/dist/src/connectors/LocalDiskConnector.d.ts.map +0 -1
- package/dist/src/connectors/LocalDiskConnector.js +0 -1233
- package/dist/src/connectors/LocalDiskConnector.js.map +0 -1
- package/docs/drive-overview.md +0 -59
- package/skills/manage-files/scripts/copy.sh +0 -7
- package/skills/manage-files/scripts/create.sh +0 -7
- package/skills/manage-files/scripts/mkdir.sh +0 -7
- package/skills/manage-files/scripts/modify.sh +0 -7
- package/skills/manage-files/scripts/upload.sh +0 -16
- package/skills/manage-files/scripts/write.sh +0 -29
- package/skills/manage-sources/scripts/reembed.sh +0 -23
|
@@ -1,1233 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import { binaryMimeTypeForPath, inferMimeTypeFromFilename } from '../domain/mime.js';
|
|
3
|
-
import { looksBinaryContent } from '../domain/text.js';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
5
|
-
import os from 'node:os';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { createHash } from 'node:crypto';
|
|
8
|
-
import { execFile } from 'node:child_process';
|
|
9
|
-
import { resolveSearchBinaries } from '../search/binaryResolver.js';
|
|
10
|
-
import { getInlineVideoCapBytes, getInlineImageCapBytes, getInlineAudioCapBytes, getInlinePdfCapBytes, getBrainConfigBoolean, getBrainConfigNumber, resolveWatchRecovery, } from '../config/env.js';
|
|
11
|
-
import { CONNECTOR_PORT_VERSION } from '@neuralis/package-system/contracts';
|
|
12
|
-
import { ConnectorRawReadTooLarge, ConnectorRootAccessError, } from '@neuralis/package-system/contracts';
|
|
13
|
-
import { discoverOsMounts } from '@neuralis/package-system/paths';
|
|
14
|
-
import { getFsWatchHub } from '@neuralis/package-system/data';
|
|
15
|
-
import { compileGlobPatterns } from '../domain/syncignore.js';
|
|
16
|
-
import { DEFAULT_MAX_SCAN_FILES } from '../domain/scanCap.js';
|
|
17
|
-
// Hard read cap — `brainMaxReadFileMb` platform key (default 20 MB), read
|
|
18
|
-
// LIVE per call. Larger files throw `File too large` for every read-path
|
|
19
|
-
// consumer (Inc 5).
|
|
20
|
-
const maxReadBytes = () => getBrainConfigNumber('brainMaxReadFileMb', undefined, 20) * 1024 * 1024;
|
|
21
|
-
/** One `readRaw` syscall's buffer slice — bounds a single read, not the file. */
|
|
22
|
-
const RAW_READ_CHUNK = 1024 * 1024;
|
|
23
|
-
// Per-MIME inline caps for binary `bytes` emission (Sidequest §2.2). Above
|
|
24
|
-
// these limits the connector returns a `[binary file too large…]` text marker
|
|
25
|
-
// instead of base64 bytes — provider context windows + tool-result caps would
|
|
26
|
-
// otherwise drown. ALL four MIME caps are LIVE platform settings now
|
|
27
|
-
// (`inlineImageMaxMb` / `inlineAudioMaxMb` / `inlinePdfMaxMb` joined
|
|
28
|
-
// `inlineVideoMaxMb` in Inc 5 — same env.ts cap-fn shape, media Inc 3′c
|
|
29
|
-
// pattern). Audio default ~8 min of 128kbps mp3 (media-that-works Inc 7).
|
|
30
|
-
// PDF text-extract cap — `brainPdfTextExcerptBytes` (default 200 KB), LIVE:
|
|
31
|
-
// enough to brief the model without exploding context. Larger PDFs get
|
|
32
|
-
// truncated with a tail marker.
|
|
33
|
-
const pdfTextExcerptBytes = () => getBrainConfigNumber('brainPdfTextExcerptBytes', undefined, 200 * 1024);
|
|
34
|
-
// First-page render output cap — `pdftoppm -r 100` on a typical page comes in
|
|
35
|
-
// well under 4 MB. Opt-in via the `fsReadPdfRender` platform key (env
|
|
36
|
-
// NEURALIS_FS_READ_PDF_RENDER fallback), read LIVE per call (Inc 4).
|
|
37
|
-
const pdfRenderFirstPage = () => getBrainConfigBoolean('fsReadPdfRender', 'NEURALIS_FS_READ_PDF_RENDER', false);
|
|
38
|
-
function formatBytes(n) {
|
|
39
|
-
if (!Number.isFinite(n) || n <= 0)
|
|
40
|
-
return '0 B';
|
|
41
|
-
if (n < 1024)
|
|
42
|
-
return `${n} B`;
|
|
43
|
-
if (n < 1024 * 1024)
|
|
44
|
-
return `${(n / 1024).toFixed(1)} KB`;
|
|
45
|
-
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
46
|
-
}
|
|
47
|
-
function inlineCapForMime(mimeType) {
|
|
48
|
-
if (mimeType === 'application/pdf')
|
|
49
|
-
return getInlinePdfCapBytes();
|
|
50
|
-
if (mimeType.startsWith('image/'))
|
|
51
|
-
return getInlineImageCapBytes();
|
|
52
|
-
if (mimeType.startsWith('video/'))
|
|
53
|
-
return getInlineVideoCapBytes();
|
|
54
|
-
if (mimeType.startsWith('audio/'))
|
|
55
|
-
return getInlineAudioCapBytes();
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
let pdftotextProbed = null;
|
|
59
|
-
let pdftoppmProbed = null;
|
|
60
|
-
/** PATH-only child env — never the host process.env (frameSample pattern):
|
|
61
|
-
* these are member-reachable spawns (`fs_read`/`fs_search`), and the app
|
|
62
|
-
* process env carries infra secrets (QDRANT_API_KEY et al.) that no helper
|
|
63
|
-
* child needs. */
|
|
64
|
-
const CHILD_ENV = { PATH: process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin' };
|
|
65
|
-
async function whichBinary(name) {
|
|
66
|
-
return new Promise((resolve) => {
|
|
67
|
-
const child = execFile('which', [name], { env: CHILD_ENV }, (err, stdout) => {
|
|
68
|
-
if (err)
|
|
69
|
-
return resolve(false);
|
|
70
|
-
resolve(typeof stdout === 'string' && stdout.trim().length > 0);
|
|
71
|
-
});
|
|
72
|
-
child.on('error', () => resolve(false));
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
async function hasPdftotext() {
|
|
76
|
-
if (pdftotextProbed === null)
|
|
77
|
-
pdftotextProbed = await whichBinary('pdftotext');
|
|
78
|
-
return pdftotextProbed;
|
|
79
|
-
}
|
|
80
|
-
async function hasPdftoppm() {
|
|
81
|
-
if (pdftoppmProbed === null)
|
|
82
|
-
pdftoppmProbed = await whichBinary('pdftoppm');
|
|
83
|
-
return pdftoppmProbed;
|
|
84
|
-
}
|
|
85
|
-
async function pdftotextExcerpt(absPath) {
|
|
86
|
-
const excerptBytes = pdfTextExcerptBytes();
|
|
87
|
-
return new Promise((resolve) => {
|
|
88
|
-
const child = execFile('pdftotext', [absPath, '-'], { timeout: 10_000, maxBuffer: excerptBytes * 4, env: CHILD_ENV }, (err, stdout) => {
|
|
89
|
-
if (err)
|
|
90
|
-
return resolve(null);
|
|
91
|
-
const out = String(stdout ?? '');
|
|
92
|
-
const trimmed = out.length > excerptBytes
|
|
93
|
-
? `${out.slice(0, excerptBytes)}\n\n…(pdftotext output truncated at ${excerptBytes} bytes)`
|
|
94
|
-
: out;
|
|
95
|
-
resolve(trimmed);
|
|
96
|
-
});
|
|
97
|
-
child.on('error', () => resolve(null));
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* THE PDF→PNG renderer (`pdftoppm -r 100`): pages `first..last` of `absPath`
|
|
102
|
-
* (clamped to the document), in page order. One private temp directory per call (`mkdtemp`), removed
|
|
103
|
-
* afterwards — concurrent renders never read each other's output. A failed or
|
|
104
|
-
* timed-out render answers `[]`. Two callers: `read` (the first page, opt-in
|
|
105
|
-
* `fsReadPdfRender`) and the sync's media lane (`renderPdfPages`).
|
|
106
|
-
*/
|
|
107
|
-
async function pdftoppmPages(absPath, first, last) {
|
|
108
|
-
const from = Math.max(1, Math.floor(first));
|
|
109
|
-
const to = Math.max(from, Math.floor(last));
|
|
110
|
-
let dir;
|
|
111
|
-
try {
|
|
112
|
-
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'neuralis-pdfrender-'));
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
return [];
|
|
116
|
-
}
|
|
117
|
-
try {
|
|
118
|
-
const ok = await new Promise((resolve) => {
|
|
119
|
-
const child = execFile('pdftoppm', ['-f', String(from), '-l', String(to), '-r', '100', '-png', absPath, path.join(dir, 'page')],
|
|
120
|
-
// 15 s per page, like the single-page render always had.
|
|
121
|
-
{ timeout: 15_000 * (to - from + 1), env: CHILD_ENV }, (err) => resolve(!err));
|
|
122
|
-
child.on('error', () => resolve(false));
|
|
123
|
-
});
|
|
124
|
-
if (!ok)
|
|
125
|
-
return [];
|
|
126
|
-
// `page-1.png`, or zero-padded (`page-01.png`) once the document has ≥ 10 pages.
|
|
127
|
-
const files = (await fs.readdir(dir))
|
|
128
|
-
.map((name) => ({ name, page: Number(/-(\d+)\.png$/.exec(name)?.[1]) }))
|
|
129
|
-
.filter((f) => Number.isFinite(f.page))
|
|
130
|
-
.sort((a, b) => a.page - b.page);
|
|
131
|
-
const pages = [];
|
|
132
|
-
for (const f of files)
|
|
133
|
-
pages.push({ page: f.page, png: await fs.readFile(path.join(dir, f.name)) });
|
|
134
|
-
return pages;
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
return [];
|
|
138
|
-
}
|
|
139
|
-
finally {
|
|
140
|
-
await fs.rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Concrete root-access error thrown by the local-disk connector. Routes +
|
|
145
|
-
* services catch the abstract base (`ConnectorRootAccessError`), not this
|
|
146
|
-
* class specifically — so every future connector that touches a real FS
|
|
147
|
-
* surface can share the same structured-warning pathway.
|
|
148
|
-
*/
|
|
149
|
-
export class LocalDiskRootAccessError extends ConnectorRootAccessError {
|
|
150
|
-
constructor(code, rootPath, uri, cause) {
|
|
151
|
-
super(code, rootPath, uri, code === 'ROOT_MISSING'
|
|
152
|
-
? `Local source root "${rootPath}" does not exist`
|
|
153
|
-
: `Local source root "${rootPath}" is not readable (permission denied)`, cause);
|
|
154
|
-
this.name = 'LocalDiskRootAccessError';
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
function isSkippableFsError(error) {
|
|
158
|
-
const code = error?.code;
|
|
159
|
-
return code === 'EACCES' || code === 'EPERM' || code === 'ENOENT';
|
|
160
|
-
}
|
|
161
|
-
function compileGlobMatchers(patterns) {
|
|
162
|
-
const compile = (value) => {
|
|
163
|
-
const escaped = value.replace(/[.+^${}()|[\]]/g, '\\$&');
|
|
164
|
-
const regex = escaped
|
|
165
|
-
.replace(/\*\*/g, '§§')
|
|
166
|
-
.replace(/\*/g, '[^/]*')
|
|
167
|
-
.replace(/§§/g, '.*')
|
|
168
|
-
.replace(/\?/g, '[^/]');
|
|
169
|
-
return new RegExp(`^${regex}$`);
|
|
170
|
-
};
|
|
171
|
-
const out = [];
|
|
172
|
-
for (const pat of patterns) {
|
|
173
|
-
const normalized = pat.replace(/\\/g, '/');
|
|
174
|
-
out.push(compile(normalized));
|
|
175
|
-
if (normalized.endsWith('/**')) {
|
|
176
|
-
out.push(compile(normalized.slice(0, -3)));
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return out;
|
|
180
|
-
}
|
|
181
|
-
function matchesAny(relPath, matchers) {
|
|
182
|
-
const candidate = relPath.replace(/\\/g, '/');
|
|
183
|
-
for (const m of matchers) {
|
|
184
|
-
if (m.test(candidate))
|
|
185
|
-
return true;
|
|
186
|
-
const parts = candidate.split('/');
|
|
187
|
-
for (let i = 0; i < parts.length; i++) {
|
|
188
|
-
const sub = parts.slice(i).join('/');
|
|
189
|
-
if (m.test(sub))
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
const SCAN_TIMEOUT_MS = 30_000;
|
|
196
|
-
const SCAN_MAX_BUFFER = 10 * 1024 * 1024;
|
|
197
|
-
const COUNT_CACHE_TTL_MS = 60_000;
|
|
198
|
-
function runBinary(bin, args, cwd, _maxMatches) {
|
|
199
|
-
// Live `brainScanTimeoutMs` (default SCAN_TIMEOUT_MS) — grep/rg kill timeout.
|
|
200
|
-
const scanTimeoutMs = getBrainConfigNumber('brainScanTimeoutMs', undefined, SCAN_TIMEOUT_MS);
|
|
201
|
-
return new Promise((resolve, reject) => {
|
|
202
|
-
execFile(bin, args, {
|
|
203
|
-
cwd,
|
|
204
|
-
timeout: scanTimeoutMs,
|
|
205
|
-
maxBuffer: SCAN_MAX_BUFFER,
|
|
206
|
-
env: CHILD_ENV,
|
|
207
|
-
}, (error, stdout, stderr) => {
|
|
208
|
-
const code = error && 'code' in error && typeof error.code === 'number' ? error.code : 0;
|
|
209
|
-
if (error && code !== 1 && code !== 0) {
|
|
210
|
-
const killed = 'killed' in error && error.killed;
|
|
211
|
-
if (killed)
|
|
212
|
-
return reject(new Error(`Search timed out after ${scanTimeoutMs}ms`));
|
|
213
|
-
return reject(new Error(`Search binary failed (${bin}): ${String(stderr || error.message)}`));
|
|
214
|
-
}
|
|
215
|
-
resolve(String(stdout ?? ''));
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
function accessErrorCode(error) {
|
|
220
|
-
const code = error?.code;
|
|
221
|
-
if (code === 'ENOENT')
|
|
222
|
-
return 'ROOT_MISSING';
|
|
223
|
-
if (code === 'EACCES' || code === 'EPERM')
|
|
224
|
-
return 'ROOT_PERMISSION_DENIED';
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
async function resolveAndGuard(basePath, target, options) {
|
|
228
|
-
const normalizedTarget = path.isAbsolute(target) ? `.${target}` : target;
|
|
229
|
-
const normalizedBase = path.resolve(basePath);
|
|
230
|
-
const rootReal = await fs.realpath(normalizedBase).catch(() => normalizedBase);
|
|
231
|
-
const resolved = path.resolve(normalizedBase, normalizedTarget);
|
|
232
|
-
const relative = path.relative(normalizedBase, resolved);
|
|
233
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
234
|
-
throw new Error(`Path traversal blocked: "${target}" escapes base "${basePath}"`);
|
|
235
|
-
}
|
|
236
|
-
const segments = relative.split(path.sep).filter(Boolean);
|
|
237
|
-
let current = rootReal;
|
|
238
|
-
for (let index = 0; index < segments.length; index += 1) {
|
|
239
|
-
const candidate = path.join(current, segments[index]);
|
|
240
|
-
try {
|
|
241
|
-
const stat = await fs.lstat(candidate);
|
|
242
|
-
if (stat.isSymbolicLink()) {
|
|
243
|
-
throw new Error(`Symlink traversal blocked: "${target}"`);
|
|
244
|
-
}
|
|
245
|
-
const real = await fs.realpath(candidate).catch(() => candidate);
|
|
246
|
-
if (!real.startsWith(rootReal + path.sep) && real !== rootReal) {
|
|
247
|
-
throw new Error(`Path traversal blocked: "${target}" escapes base "${basePath}"`);
|
|
248
|
-
}
|
|
249
|
-
current = real;
|
|
250
|
-
}
|
|
251
|
-
catch (error) {
|
|
252
|
-
if (error.code === 'ENOENT' && options?.allowMissingTail) {
|
|
253
|
-
return path.join(current, ...segments.slice(index));
|
|
254
|
-
}
|
|
255
|
-
throw error;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return current;
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* The capability set the local on-disk connector implements. This MUST stay
|
|
262
|
-
* byte-equal to the `local` connector's `capabilities` array in
|
|
263
|
-
* `package.json#neuralis.connectors` — `SourceLoader.instantiateConnector`
|
|
264
|
-
* fails closed (`ConnectorCapabilityMismatch`) on any divergence, and for a
|
|
265
|
-
* first-party connector `wiring.ts` now re-throws that (boot-fatal) instead of
|
|
266
|
-
* silently degrading the kind. `localDiskCapabilities.test.ts` is the
|
|
267
|
-
* derive-and-verify drift guard that catches a mismatch at TEST time.
|
|
268
|
-
*/
|
|
269
|
-
export const LOCAL_DISK_CAPABILITIES = [
|
|
270
|
-
'list', 'read', 'stat', 'write', 'delete', 'move', 'mkdir', 'rmdir',
|
|
271
|
-
'scanDelta', 'walk', 'count', 'peekCount', 'scanContent', 'subscribeEvents', 'resolveOsUri',
|
|
272
|
-
'rawRead',
|
|
273
|
-
];
|
|
274
|
-
/**
|
|
275
|
-
* Structured failure thrown by `LocalDiskConnector.resolveOsUri` when the
|
|
276
|
-
* connector is running inside a Docker container but no host-pair root has
|
|
277
|
-
* been configured. Returning a container-internal path here would corrupt
|
|
278
|
-
* vector payloads + filesystem-node `os_uri` fields with paths that do not
|
|
279
|
-
* exist on the host. Callers (sync runner, the write engine) catch this
|
|
280
|
-
* and either skip the upsert or persist `os_uri: undefined` instead.
|
|
281
|
-
*/
|
|
282
|
-
export class OsUriUnresolvableError extends Error {
|
|
283
|
-
code;
|
|
284
|
-
source;
|
|
285
|
-
uri;
|
|
286
|
-
constructor(params) {
|
|
287
|
-
super(`os_uri unresolvable for "${params.uri}" on source "${params.source}": ` +
|
|
288
|
-
`running in Docker, but the source config has no \`hostRoot\` and no ` +
|
|
289
|
-
`\`NEURALIS_MOUNT_*\` os-mount covers its root` +
|
|
290
|
-
(params.root ? ` ("${params.root}")` : '') +
|
|
291
|
-
`. Refusing to emit container-internal paths into vectors/os_uri. ` +
|
|
292
|
-
`Add the path via \`neuralis mount add\` (which registers the ` +
|
|
293
|
-
`host-pair env), or set \`hostRoot\` on the source config.`);
|
|
294
|
-
this.name = 'OsUriUnresolvableError';
|
|
295
|
-
this.code = 'os_uri_host_pair_missing';
|
|
296
|
-
this.source = params.source;
|
|
297
|
-
this.uri = params.uri;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Connector for local filesystem access — URI-native.
|
|
302
|
-
*
|
|
303
|
-
* Contributed by brain-core via `neuralis.connectors[]` manifest entry
|
|
304
|
-
* `{ kind: 'local', configSchema: { root, hostRoot? } }`. The package loader
|
|
305
|
-
* instantiates it via `createLocalDiskConnector(ctx)` for each source binding.
|
|
306
|
-
*/
|
|
307
|
-
export class LocalDiskConnector {
|
|
308
|
-
kind = 'local';
|
|
309
|
-
source;
|
|
310
|
-
portVersion = CONNECTOR_PORT_VERSION;
|
|
311
|
-
#root;
|
|
312
|
-
#hostRoot;
|
|
313
|
-
#isDocker;
|
|
314
|
-
#capabilities;
|
|
315
|
-
#countCache = new Map();
|
|
316
|
-
constructor(root, source, hostRoot, isDocker) {
|
|
317
|
-
this.#root = path.resolve(root);
|
|
318
|
-
this.source = source;
|
|
319
|
-
this.#hostRoot = hostRoot ? path.resolve(hostRoot) : undefined;
|
|
320
|
-
this.#isDocker = isDocker ?? detectDockerRuntime();
|
|
321
|
-
this.#capabilities = Object.freeze(new Set(LOCAL_DISK_CAPABILITIES));
|
|
322
|
-
}
|
|
323
|
-
capabilities() {
|
|
324
|
-
return this.#capabilities;
|
|
325
|
-
}
|
|
326
|
-
/** Expose the resolved root path for diagnostics / wiring logs. */
|
|
327
|
-
get root() {
|
|
328
|
-
return this.#root;
|
|
329
|
-
}
|
|
330
|
-
async probeRoot() {
|
|
331
|
-
try {
|
|
332
|
-
await fs.access(this.#root, (await import('node:fs')).constants.R_OK);
|
|
333
|
-
return { rootPath: this.#root, exists: true, readable: true };
|
|
334
|
-
}
|
|
335
|
-
catch (error) {
|
|
336
|
-
const code = accessErrorCode(error);
|
|
337
|
-
return {
|
|
338
|
-
rootPath: this.#root,
|
|
339
|
-
exists: code !== 'ROOT_MISSING',
|
|
340
|
-
readable: false,
|
|
341
|
-
error: error instanceof Error ? error.message : String(error),
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Connector-owned liveness — whether the backing host directory is reachable.
|
|
347
|
-
* Wraps {@link probeRoot} but maps to the fixed `SourceLiveReason` enum and
|
|
348
|
-
* DISCARDS the raw `rootPath`/`error` (those carry the absolute host-mount
|
|
349
|
-
* path — leaking them to a member via the tone-title is a path-disclosure
|
|
350
|
-
* regression). UI-tone + `<runtime_stack>` signal only; never gates access.
|
|
351
|
-
*/
|
|
352
|
-
async probeConnection() {
|
|
353
|
-
const probe = await this.probeRoot();
|
|
354
|
-
if (probe.exists && probe.readable)
|
|
355
|
-
return { connected: true };
|
|
356
|
-
return { connected: false, reason: probe.exists ? 'fs_permission' : 'fs_unreachable' };
|
|
357
|
-
}
|
|
358
|
-
#extractPath(uri) {
|
|
359
|
-
const prefix = `${this.source}://`;
|
|
360
|
-
if (!uri.startsWith(prefix)) {
|
|
361
|
-
throw new Error(`URI "${uri}" does not belong to source "${this.source}"`);
|
|
362
|
-
}
|
|
363
|
-
return uri.slice(prefix.length) || '/';
|
|
364
|
-
}
|
|
365
|
-
#toUri(relativePath) {
|
|
366
|
-
const clean = relativePath.startsWith('/') ? relativePath.slice(1) : relativePath;
|
|
367
|
-
return clean ? `${this.source}://${clean}` : `${this.source}://`;
|
|
368
|
-
}
|
|
369
|
-
async list(uri) {
|
|
370
|
-
const dirPath = this.#extractPath(uri);
|
|
371
|
-
const isRoot = dirPath === '/' || dirPath === '' || dirPath === '.';
|
|
372
|
-
let absPath;
|
|
373
|
-
try {
|
|
374
|
-
absPath = await resolveAndGuard(this.#root, isRoot ? '.' : dirPath);
|
|
375
|
-
}
|
|
376
|
-
catch (error) {
|
|
377
|
-
const code = accessErrorCode(error);
|
|
378
|
-
if (isRoot && code) {
|
|
379
|
-
throw new LocalDiskRootAccessError(code, this.#root, uri, error);
|
|
380
|
-
}
|
|
381
|
-
if (isSkippableFsError(error))
|
|
382
|
-
return [];
|
|
383
|
-
throw error;
|
|
384
|
-
}
|
|
385
|
-
let entries;
|
|
386
|
-
try {
|
|
387
|
-
entries = await fs.readdir(absPath, { withFileTypes: true });
|
|
388
|
-
}
|
|
389
|
-
catch (error) {
|
|
390
|
-
const code = accessErrorCode(error);
|
|
391
|
-
if (isRoot && code) {
|
|
392
|
-
throw new LocalDiskRootAccessError(code, this.#root, uri, error);
|
|
393
|
-
}
|
|
394
|
-
if (isSkippableFsError(error))
|
|
395
|
-
return [];
|
|
396
|
-
throw error;
|
|
397
|
-
}
|
|
398
|
-
const items = [];
|
|
399
|
-
for (const entry of entries) {
|
|
400
|
-
if (entry.isSymbolicLink())
|
|
401
|
-
continue;
|
|
402
|
-
const entryRelPath = path.join(dirPath === '/' ? '/' : dirPath, entry.name);
|
|
403
|
-
const entryUri = this.#toUri(entryRelPath);
|
|
404
|
-
const isDir = entry.isDirectory();
|
|
405
|
-
let size;
|
|
406
|
-
let modifiedAt;
|
|
407
|
-
if (!isDir) {
|
|
408
|
-
try {
|
|
409
|
-
const stat = await fs.stat(path.join(absPath, entry.name));
|
|
410
|
-
size = stat.size;
|
|
411
|
-
modifiedAt = stat.mtime.toISOString();
|
|
412
|
-
}
|
|
413
|
-
catch { /* stat failure is non-fatal for listing */ }
|
|
414
|
-
}
|
|
415
|
-
items.push({
|
|
416
|
-
uri: entryUri,
|
|
417
|
-
name: entry.name,
|
|
418
|
-
isDirectory: isDir,
|
|
419
|
-
...(size != null ? { size } : {}),
|
|
420
|
-
...(modifiedAt ? { modifiedAt } : {}),
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
return items.sort((a, b) => {
|
|
424
|
-
if (a.isDirectory !== b.isDirectory)
|
|
425
|
-
return a.isDirectory ? -1 : 1;
|
|
426
|
-
return a.name.localeCompare(b.name);
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Existence + metadata, on the SAME guarded path as `read` and without
|
|
431
|
-
* transferring a byte of content. `null` = the path resolved but nothing is
|
|
432
|
-
* there (`ENOENT`, from either the guard's per-segment walk or the `lstat`);
|
|
433
|
-
* every other failure THROWS exactly as `read` would — a traversal/symlink
|
|
434
|
-
* refusal, an EACCES, a root-access error — so the caller's existing
|
|
435
|
-
* "gone vs. could-not-ask" split keeps working unchanged.
|
|
436
|
-
*
|
|
437
|
-
* `lstat`, never `stat`: `resolveAndGuard` REFUSES a symlink rather than
|
|
438
|
-
* resolving through it, so following one here would answer for a target the
|
|
439
|
-
* read path would never have served.
|
|
440
|
-
*/
|
|
441
|
-
async stat(uri) {
|
|
442
|
-
const ref = this.#extractPath(uri);
|
|
443
|
-
let absPath;
|
|
444
|
-
try {
|
|
445
|
-
absPath = await resolveAndGuard(this.#root, ref);
|
|
446
|
-
}
|
|
447
|
-
catch (error) {
|
|
448
|
-
if (error?.code === 'ENOENT')
|
|
449
|
-
return null;
|
|
450
|
-
throw error;
|
|
451
|
-
}
|
|
452
|
-
let info;
|
|
453
|
-
try {
|
|
454
|
-
info = await fs.lstat(absPath);
|
|
455
|
-
}
|
|
456
|
-
catch (error) {
|
|
457
|
-
if (error?.code === 'ENOENT')
|
|
458
|
-
return null;
|
|
459
|
-
throw error;
|
|
460
|
-
}
|
|
461
|
-
return {
|
|
462
|
-
isDirectory: info.isDirectory(),
|
|
463
|
-
size: info.size,
|
|
464
|
-
modifiedAt: info.mtime.toISOString(),
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* The sync media lane's PDF read — NOT a kernel `ConnectorPort` member (only
|
|
469
|
-
* a connector that holds the file on its own disk can render it; the lane
|
|
470
|
-
* treats a connector without it as "unavailable"). The raw bytes' sha256
|
|
471
|
-
* (the point's `content_hash`) and pages `first..last` rendered by the ONE
|
|
472
|
-
* renderer — none when the hash equals `unlessHash`, so an unchanged PDF is
|
|
473
|
-
* never re-rendered. `null` when `pdftoppm` is not installed; the same
|
|
474
|
-
* containment and read cap as `read`.
|
|
475
|
-
*/
|
|
476
|
-
async renderPdfPages(uri, opts) {
|
|
477
|
-
const absPath = await resolveAndGuard(this.#root, this.#extractPath(uri));
|
|
478
|
-
const stat = await fs.stat(absPath);
|
|
479
|
-
if (stat.isDirectory())
|
|
480
|
-
throw new Error(`Cannot read directory as file: "${uri}"`);
|
|
481
|
-
const readCap = maxReadBytes();
|
|
482
|
-
if (stat.size > readCap)
|
|
483
|
-
throw new Error(`File too large (${stat.size} bytes, max ${readCap}): "${uri}"`);
|
|
484
|
-
if (!(await hasPdftoppm()))
|
|
485
|
-
return null;
|
|
486
|
-
const contentHash = createHash('sha256').update(await fs.readFile(absPath)).digest('hex');
|
|
487
|
-
if (opts.unlessHash !== undefined && opts.unlessHash === contentHash)
|
|
488
|
-
return { contentHash, pages: [] };
|
|
489
|
-
return { contentHash, pages: await pdftoppmPages(absPath, opts.first, opts.last) };
|
|
490
|
-
}
|
|
491
|
-
/**
|
|
492
|
-
* The byte-exact read. Same guarded resolver as `read`; the cap is
|
|
493
|
-
* `min(opts.maxBytes, brainMaxReadFileMb)`, checked on the OPENED
|
|
494
|
-
* descriptor's size before any byte is buffered, and the read itself pulls
|
|
495
|
-
* at most that size+1 bytes — a file that grew after the size check is
|
|
496
|
-
* refused (over the cap: the too-large refusal), never cut.
|
|
497
|
-
*/
|
|
498
|
-
async readRaw(uri, opts) {
|
|
499
|
-
const absPath = await resolveAndGuard(this.#root, this.#extractPath(uri));
|
|
500
|
-
const planeCap = maxReadBytes();
|
|
501
|
-
const cap = Math.max(0, Math.min(Math.floor(opts.maxBytes), planeCap));
|
|
502
|
-
const tooLarge = (size) => new ConnectorRawReadTooLarge(uri, cap, {
|
|
503
|
-
...(size === undefined ? {} : { size }),
|
|
504
|
-
...(planeCap < opts.maxBytes ? { planeLimit: planeCap } : {}),
|
|
505
|
-
});
|
|
506
|
-
const handle = await fs.open(absPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
507
|
-
try {
|
|
508
|
-
const info = await handle.stat();
|
|
509
|
-
if (info.isDirectory())
|
|
510
|
-
throw new Error(`Cannot read directory as file: "${uri}"`);
|
|
511
|
-
if (!info.isFile())
|
|
512
|
-
throw new Error(`Not a regular file: "${uri}"`);
|
|
513
|
-
if (info.size > cap)
|
|
514
|
-
throw tooLarge(info.size);
|
|
515
|
-
// Sized by the fstat answer, not the cap; the one extra byte detects growth.
|
|
516
|
-
const limit = info.size;
|
|
517
|
-
const buffer = Buffer.allocUnsafe(limit + 1);
|
|
518
|
-
let filled = 0;
|
|
519
|
-
while (filled <= limit) {
|
|
520
|
-
const { bytesRead } = await handle.read(buffer, filled, Math.min(RAW_READ_CHUNK, limit + 1 - filled), filled);
|
|
521
|
-
if (bytesRead === 0)
|
|
522
|
-
break;
|
|
523
|
-
filled += bytesRead;
|
|
524
|
-
}
|
|
525
|
-
if (filled > limit)
|
|
526
|
-
throw filled > cap ? tooLarge() : new Error(`File changed while reading: "${uri}"`);
|
|
527
|
-
return {
|
|
528
|
-
bytes: new Uint8Array(buffer.buffer, buffer.byteOffset, filled),
|
|
529
|
-
size: filled,
|
|
530
|
-
mimeType: binaryMimeTypeForPath(absPath) ?? inferMimeTypeFromFilename(absPath),
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
finally {
|
|
534
|
-
await handle.close();
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
async read(uri) {
|
|
538
|
-
const ref = this.#extractPath(uri);
|
|
539
|
-
const absPath = await resolveAndGuard(this.#root, ref);
|
|
540
|
-
const stat = await fs.stat(absPath);
|
|
541
|
-
if (stat.isDirectory()) {
|
|
542
|
-
throw new Error(`Cannot read directory as file: "${uri}"`);
|
|
543
|
-
}
|
|
544
|
-
const readCap = maxReadBytes();
|
|
545
|
-
if (stat.size > readCap) {
|
|
546
|
-
throw new Error(`File too large (${stat.size} bytes, max ${readCap}): "${uri}"`);
|
|
547
|
-
}
|
|
548
|
-
const binaryMimeType = binaryMimeTypeForPath(absPath);
|
|
549
|
-
if (binaryMimeType) {
|
|
550
|
-
// PDFs get a special path: extract text by default (pdftotext), and
|
|
551
|
-
// optionally render the first page when NEURALIS_FS_READ_PDF_RENDER=1.
|
|
552
|
-
// The bytes emission for the rendered page goes through the same cap as
|
|
553
|
-
// images.
|
|
554
|
-
if (binaryMimeType === 'application/pdf') {
|
|
555
|
-
const haveText = await hasPdftotext();
|
|
556
|
-
const textExcerpt = haveText ? await pdftotextExcerpt(absPath) : null;
|
|
557
|
-
if (pdfRenderFirstPage() && (await hasPdftoppm())) {
|
|
558
|
-
const rendered = (await pdftoppmPages(absPath, 1, 1))[0]?.png ?? null;
|
|
559
|
-
if (rendered && rendered.length <= getInlineImageCapBytes()) {
|
|
560
|
-
return {
|
|
561
|
-
text: textExcerpt ?? '',
|
|
562
|
-
mimeType: 'image/png',
|
|
563
|
-
// Signal the rendered-PDF case: the bytes are a png rendering of
|
|
564
|
-
// a .pdf source. The journal must persist these INLINE (re-reading
|
|
565
|
-
// the .pdf uri would re-render non-deterministically / 400). dynamic-brook MM2.
|
|
566
|
-
sourceMimeType: 'application/pdf',
|
|
567
|
-
size: stat.size,
|
|
568
|
-
bytes: rendered.toString('base64'),
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
if (textExcerpt !== null) {
|
|
573
|
-
return { text: textExcerpt, mimeType: binaryMimeType, size: stat.size };
|
|
574
|
-
}
|
|
575
|
-
return {
|
|
576
|
-
text: `[application/pdf — pdftotext unavailable; install poppler-utils to enable PDF text extraction]`,
|
|
577
|
-
mimeType: binaryMimeType,
|
|
578
|
-
size: stat.size,
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
// Image / video / other binaries — emit base64 when under the per-MIME
|
|
582
|
-
// inline cap, or a text marker otherwise.
|
|
583
|
-
const cap = inlineCapForMime(binaryMimeType);
|
|
584
|
-
if (cap !== null && stat.size <= cap) {
|
|
585
|
-
const buffer = await fs.readFile(absPath);
|
|
586
|
-
return {
|
|
587
|
-
text: '',
|
|
588
|
-
mimeType: binaryMimeType,
|
|
589
|
-
size: stat.size,
|
|
590
|
-
bytes: buffer.toString('base64'),
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
if (cap !== null) {
|
|
594
|
-
return {
|
|
595
|
-
text: `[binary file too large to inline: ${binaryMimeType} ${formatBytes(stat.size)} (cap ${formatBytes(cap)})]`,
|
|
596
|
-
mimeType: binaryMimeType,
|
|
597
|
-
size: stat.size,
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
// Other binaries (zip, exe, font, …) — leave bytes unset, stay
|
|
601
|
-
// text-empty as before so the use-case downgrades to a marker.
|
|
602
|
-
return {
|
|
603
|
-
text: '',
|
|
604
|
-
mimeType: binaryMimeType,
|
|
605
|
-
size: stat.size,
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
const buffer = await fs.readFile(absPath);
|
|
609
|
-
try {
|
|
610
|
-
const text = new TextDecoder('utf-8', { fatal: true }).decode(buffer);
|
|
611
|
-
if (!looksBinaryContent(text))
|
|
612
|
-
return { text, size: stat.size };
|
|
613
|
-
}
|
|
614
|
-
catch (error) {
|
|
615
|
-
if (!(error instanceof TypeError))
|
|
616
|
-
throw error;
|
|
617
|
-
}
|
|
618
|
-
return { text: `[binary or invalid UTF-8 @ ${uri}: ${stat.size} bytes]`,
|
|
619
|
-
mimeType: 'application/octet-stream', size: stat.size };
|
|
620
|
-
}
|
|
621
|
-
async write(uri, content) {
|
|
622
|
-
const filePath = this.#extractPath(uri);
|
|
623
|
-
const absPath = await resolveAndGuard(this.#root, filePath, { allowMissingTail: true });
|
|
624
|
-
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
625
|
-
if (typeof content === 'string') {
|
|
626
|
-
await fs.writeFile(absPath, content, 'utf-8');
|
|
627
|
-
}
|
|
628
|
-
else {
|
|
629
|
-
await fs.writeFile(absPath, Buffer.from(content));
|
|
630
|
-
}
|
|
631
|
-
return { uri };
|
|
632
|
-
}
|
|
633
|
-
async delete(uri) {
|
|
634
|
-
const ref = this.#extractPath(uri);
|
|
635
|
-
const absPath = await resolveAndGuard(this.#root, ref);
|
|
636
|
-
await fs.unlink(absPath);
|
|
637
|
-
}
|
|
638
|
-
async move(fromUri, toUri) {
|
|
639
|
-
const fromRef = this.#extractPath(fromUri);
|
|
640
|
-
const toRef = this.#extractPath(toUri);
|
|
641
|
-
const fromAbs = await resolveAndGuard(this.#root, fromRef);
|
|
642
|
-
const toAbs = await resolveAndGuard(this.#root, toRef, { allowMissingTail: true });
|
|
643
|
-
await fs.mkdir(path.dirname(toAbs), { recursive: true });
|
|
644
|
-
await fs.rename(fromAbs, toAbs);
|
|
645
|
-
return { uri: toUri, previousUri: fromUri };
|
|
646
|
-
}
|
|
647
|
-
async mkdir(uri) {
|
|
648
|
-
const ref = this.#extractPath(uri);
|
|
649
|
-
const absPath = await resolveAndGuard(this.#root, ref, { allowMissingTail: true });
|
|
650
|
-
await fs.mkdir(absPath, { recursive: true });
|
|
651
|
-
}
|
|
652
|
-
async rmdir(uri) {
|
|
653
|
-
const ref = this.#extractPath(uri);
|
|
654
|
-
const absPath = await resolveAndGuard(this.#root, ref);
|
|
655
|
-
// Empty-dir removal only — the recursive folder-delete usecase unlinks /
|
|
656
|
-
// tombstones every contained file first, then drops the empty shells. A
|
|
657
|
-
// populated dir throws ENOTEMPTY here (fail-safe, never a silent blind erase).
|
|
658
|
-
await fs.rmdir(absPath);
|
|
659
|
-
}
|
|
660
|
-
/**
|
|
661
|
-
* OS-level change events under `rootUri`, fed by the shared FsWatchHub
|
|
662
|
-
* (D6: ONE watcher substrate). Catches EVERY background change — shell
|
|
663
|
-
* writes, host edits, git checkouts, external programs — not just app
|
|
664
|
-
* writes. URI mapping happens here (the hub only knows absolute paths);
|
|
665
|
-
* exclude filtering and debounce are the SUBSCRIBER's job. An `overflow`
|
|
666
|
-
* event means "event delivery can no longer be trusted — full reconcile".
|
|
667
|
-
*/
|
|
668
|
-
subscribeEvents(rootUri, listener, opts) {
|
|
669
|
-
const subPath = this.#extractPath(rootUri);
|
|
670
|
-
const watchRoot = path.resolve(this.#root, subPath === '/' ? '.' : subPath);
|
|
671
|
-
if (watchRoot !== this.#root && !watchRoot.startsWith(`${this.#root}${path.sep}`)) {
|
|
672
|
-
throw new Error(`subscribeEvents root "${rootUri}" escapes source "${this.source}".`);
|
|
673
|
-
}
|
|
674
|
-
// Push the exclude set down to the watcher: excluded trees never consume
|
|
675
|
-
// OS watch descriptors (inotify budgets matter on repo-sized sources).
|
|
676
|
-
const excludeRegexes = compileGlobPatterns(opts?.excludeGlobs ?? []);
|
|
677
|
-
const ignored = excludeRegexes.length > 0
|
|
678
|
-
? (absPath) => {
|
|
679
|
-
const rel = path.relative(this.#root, absPath);
|
|
680
|
-
if (!rel || rel.startsWith('..'))
|
|
681
|
-
return false;
|
|
682
|
-
const relPosix = rel.split(path.sep).join('/');
|
|
683
|
-
return excludeRegexes.some((rx) => rx.test(relPosix));
|
|
684
|
-
}
|
|
685
|
-
: undefined;
|
|
686
|
-
const handle = getFsWatchHub().acquire(watchRoot, (event) => {
|
|
687
|
-
if (event.kind === 'overflow') {
|
|
688
|
-
// Carry the hub's classification through 1:1 — a fixed reason class
|
|
689
|
-
// and an errno NAME, never a path or an error message (invariant 9).
|
|
690
|
-
// The consumer still ACTS the same way whatever the value (reconcile
|
|
691
|
-
// the whole source); the cause exists so an incident can be attributed
|
|
692
|
-
// from a log line instead of being one undifferentiated signal.
|
|
693
|
-
listener({
|
|
694
|
-
kind: 'overflow',
|
|
695
|
-
uri: `${this.source}://`,
|
|
696
|
-
modifiedAt: event.at,
|
|
697
|
-
...(event.reason ? { reason: event.reason } : {}),
|
|
698
|
-
...(event.code ? { code: event.code } : {}),
|
|
699
|
-
});
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
const rel = path.relative(this.#root, event.absPath);
|
|
703
|
-
if (!rel || rel.startsWith('..'))
|
|
704
|
-
return;
|
|
705
|
-
listener({
|
|
706
|
-
kind: event.kind,
|
|
707
|
-
uri: this.#toUri(rel.split(path.sep).join('/')),
|
|
708
|
-
modifiedAt: event.at,
|
|
709
|
-
});
|
|
710
|
-
}, {
|
|
711
|
-
...(ignored ? { ignored } : {}),
|
|
712
|
-
// Declared per listener: the hub shares one watcher per root and takes
|
|
713
|
-
// the conservative maximum across everything live on it.
|
|
714
|
-
recovery: resolveWatchRecovery(),
|
|
715
|
-
});
|
|
716
|
-
return {
|
|
717
|
-
unsubscribe: () => { void handle.release(); },
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
async scanDelta(request) {
|
|
721
|
-
const rootUri = request.rootUri ?? `${this.source}://`;
|
|
722
|
-
const rootPath = this.#extractPath(rootUri) || '/';
|
|
723
|
-
// The checkpoint is stamped BEFORE the walk, never after it. A walk of a
|
|
724
|
-
// repo-sized tree takes seconds to minutes; a file edited AFTER the walk
|
|
725
|
-
// visited it but BEFORE the walk ended carries an mtime earlier than an
|
|
726
|
-
// end-stamped cursor, so the next delta would never report it and the edit
|
|
727
|
-
// would sit unindexed until something else touched the file. Stamping first
|
|
728
|
-
// makes the overlap conservative: the window re-reports, it never skips.
|
|
729
|
-
const checkpoint = new Date().toISOString();
|
|
730
|
-
const { items: files, truncated } = await this.#walkFiles(rootPath, request.signal, request.excludeGlobs, request.maxScanFiles);
|
|
731
|
-
// An unparseable cursor yields NaN, and every `>=` against NaN is false —
|
|
732
|
-
// which would report NOTHING changed and silently strand the whole source.
|
|
733
|
-
// Treat it as "no cursor" and return the full set: this scan re-reports,
|
|
734
|
-
// it never skips. (HEAD relied on NaN being falsy for the same outcome;
|
|
735
|
-
// the explicit test survives the `>= 0` boundary the inclusive bound has.)
|
|
736
|
-
const parsedSince = request.since ? new Date(request.since).getTime() : undefined;
|
|
737
|
-
const sinceMs = parsedSince !== undefined && Number.isFinite(parsedSince) ? parsedSince : undefined;
|
|
738
|
-
// INCLUSIVE (`>=`) on purpose. Filesystem mtime resolution is coarser than
|
|
739
|
-
// the cursor (whole seconds on several filesystems), so a change landing in
|
|
740
|
-
// the same tick as the previous checkpoint is indistinguishable from the
|
|
741
|
-
// state that checkpoint described. An exclusive bound drops it silently;
|
|
742
|
-
// the inclusive one re-reports the boundary file and the content-hash skip
|
|
743
|
-
// in the sync runner absorbs the repeat at no embedding cost.
|
|
744
|
-
const changed = sinceMs === undefined ? files : files.filter((item) => {
|
|
745
|
-
if (!item.modifiedAt)
|
|
746
|
-
return true;
|
|
747
|
-
const modifiedAtMs = new Date(item.modifiedAt).getTime();
|
|
748
|
-
return Number.isFinite(modifiedAtMs) && modifiedAtMs >= sinceMs;
|
|
749
|
-
});
|
|
750
|
-
return {
|
|
751
|
-
files: changed,
|
|
752
|
-
checkpoint,
|
|
753
|
-
truncated,
|
|
754
|
-
// ONE walk answers both questions: `changed` is the delta, `files` is the
|
|
755
|
-
// complete set the same traversal saw, and `checkpoint`/`truncated`
|
|
756
|
-
// describe both. Answered only when asked — an unasked inventory would
|
|
757
|
-
// double the response for every caller that does not need it.
|
|
758
|
-
...(request.includeInventory ? { inventory: files } : {}),
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
resolveOsUri(uri) {
|
|
762
|
-
const relativePath = this.#extractPath(uri);
|
|
763
|
-
const absPath = path.resolve(this.#root, relativePath.startsWith('/') ? `.${relativePath}` : relativePath);
|
|
764
|
-
if (this.#hostRoot) {
|
|
765
|
-
return absPath.replace(this.#root, this.#hostRoot);
|
|
766
|
-
}
|
|
767
|
-
if (this.#isDocker) {
|
|
768
|
-
// Running in Docker without a host-pair root → returning the
|
|
769
|
-
// container-internal absPath would write a path that does not exist
|
|
770
|
-
// on the host into the vector payload and the filesystem-node
|
|
771
|
-
// os_uri field. Fail loudly so the caller can skip / null it out.
|
|
772
|
-
throw new OsUriUnresolvableError({ source: this.source, uri, root: this.#root });
|
|
773
|
-
}
|
|
774
|
-
return absPath;
|
|
775
|
-
}
|
|
776
|
-
#countCacheKey(rootUri, opts) {
|
|
777
|
-
// No default excludes — physical count MUST reflect the real filesystem.
|
|
778
|
-
// Callers that want filtered counts pass opts.excludeGlobs explicitly.
|
|
779
|
-
const excludes = opts?.excludeGlobs ?? [];
|
|
780
|
-
const cap = typeof opts?.maxScanFiles === 'number' && opts.maxScanFiles > 0 ? opts.maxScanFiles : DEFAULT_MAX_SCAN_FILES;
|
|
781
|
-
return `${rootUri}::${excludes.join('|')}::${cap}`;
|
|
782
|
-
}
|
|
783
|
-
peekCount(rootUri, opts) {
|
|
784
|
-
const cached = this.#countCache.get(this.#countCacheKey(rootUri, opts));
|
|
785
|
-
if (!cached)
|
|
786
|
-
return undefined;
|
|
787
|
-
if (Date.now() - cached.ts >= getBrainConfigNumber('brainCountCacheTtlMs', undefined, COUNT_CACHE_TTL_MS))
|
|
788
|
-
return undefined;
|
|
789
|
-
return cached.value;
|
|
790
|
-
}
|
|
791
|
-
async walk(rootUri, opts) {
|
|
792
|
-
return (await this.#walk(rootUri, opts, false)).items;
|
|
793
|
-
}
|
|
794
|
-
async walkWithStatus(rootUri, opts) {
|
|
795
|
-
return this.#walk(rootUri, opts, true);
|
|
796
|
-
}
|
|
797
|
-
async #walk(rootUri, opts, diagnostic) {
|
|
798
|
-
// Strip a trailing slash on the walk root so a caller-supplied
|
|
799
|
-
// `packages://x/` does not seed `current.virtual = "x/"` and produce
|
|
800
|
-
// mid-URI "//" children via the string-concat below. `list()`/`#walkFiles()`
|
|
801
|
-
// use `path.join` and are already safe; this keeps `walk()` robust too.
|
|
802
|
-
const rootPath = (this.#extractPath(rootUri) || '/').replace(/\/+$/, '') || '/';
|
|
803
|
-
let absRoot;
|
|
804
|
-
try {
|
|
805
|
-
absRoot = await resolveAndGuard(this.#root, rootPath === '/' ? '.' : rootPath);
|
|
806
|
-
}
|
|
807
|
-
catch (error) {
|
|
808
|
-
if (diagnostic || isSkippableFsError(error))
|
|
809
|
-
return { items: [], complete: false, truncated: false };
|
|
810
|
-
throw error;
|
|
811
|
-
}
|
|
812
|
-
const maxItems = typeof opts?.maxItems === 'number' && opts.maxItems > 0
|
|
813
|
-
? Math.min(opts.maxItems, DEFAULT_MAX_SCAN_FILES)
|
|
814
|
-
: diagnostic ? DEFAULT_MAX_SCAN_FILES : 500;
|
|
815
|
-
const maxEntries = typeof opts?.maxEntries === 'number' && Number.isFinite(opts.maxEntries) && opts.maxEntries > 0
|
|
816
|
-
? Math.min(Math.floor(opts.maxEntries), DEFAULT_MAX_SCAN_FILES)
|
|
817
|
-
: typeof opts?.maxItems === 'number' && Number.isFinite(opts.maxItems) && opts.maxItems > 0
|
|
818
|
-
? Math.min(Math.floor(opts.maxItems), DEFAULT_MAX_SCAN_FILES) : 500;
|
|
819
|
-
let visited = 1; // The traversal root is a visited directory too.
|
|
820
|
-
let complete = true;
|
|
821
|
-
let truncated = false;
|
|
822
|
-
// No default excludes — walk returns the full on-disk tree.
|
|
823
|
-
const excludes = opts?.excludeGlobs ?? [];
|
|
824
|
-
const excludeMatchers = compileGlobMatchers(excludes);
|
|
825
|
-
const items = [];
|
|
826
|
-
const queue = [
|
|
827
|
-
{ abs: absRoot, virtual: rootPath === '/' ? '/' : rootPath },
|
|
828
|
-
];
|
|
829
|
-
let nextDirectory = 0;
|
|
830
|
-
while (nextDirectory < queue.length) {
|
|
831
|
-
if (items.length >= maxItems || (diagnostic && visited >= maxEntries)) {
|
|
832
|
-
truncated = true;
|
|
833
|
-
break;
|
|
834
|
-
}
|
|
835
|
-
const current = queue[nextDirectory++];
|
|
836
|
-
let entries;
|
|
837
|
-
try {
|
|
838
|
-
if (diagnostic) {
|
|
839
|
-
// One-entry native buffer and a charged application buffer: no full
|
|
840
|
-
// readdir allocation before the caller's budget has been applied.
|
|
841
|
-
const directory = await fs.opendir(current.abs, { bufferSize: 1 });
|
|
842
|
-
entries = [];
|
|
843
|
-
try {
|
|
844
|
-
while (visited < maxEntries) {
|
|
845
|
-
const entry = await directory.read();
|
|
846
|
-
if (entry === null)
|
|
847
|
-
break;
|
|
848
|
-
visited += 1;
|
|
849
|
-
entries.push(entry);
|
|
850
|
-
}
|
|
851
|
-
if (visited >= maxEntries)
|
|
852
|
-
truncated = true;
|
|
853
|
-
}
|
|
854
|
-
finally {
|
|
855
|
-
await directory.close();
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
else {
|
|
859
|
-
entries = await fs.readdir(current.abs, { withFileTypes: true });
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
catch (error) {
|
|
863
|
-
if (diagnostic || isSkippableFsError(error)) {
|
|
864
|
-
complete = false;
|
|
865
|
-
continue;
|
|
866
|
-
}
|
|
867
|
-
throw error;
|
|
868
|
-
}
|
|
869
|
-
entries.sort((a, b) => {
|
|
870
|
-
const aDir = a.isDirectory() ? 0 : 1;
|
|
871
|
-
const bDir = b.isDirectory() ? 0 : 1;
|
|
872
|
-
if (aDir !== bDir)
|
|
873
|
-
return aDir - bDir;
|
|
874
|
-
return a.name.localeCompare(b.name);
|
|
875
|
-
});
|
|
876
|
-
for (const entry of entries) {
|
|
877
|
-
if (items.length >= maxItems) {
|
|
878
|
-
truncated = true;
|
|
879
|
-
break;
|
|
880
|
-
}
|
|
881
|
-
if (entry.isSymbolicLink?.()) {
|
|
882
|
-
complete = false;
|
|
883
|
-
continue;
|
|
884
|
-
}
|
|
885
|
-
const entryAbs = path.join(current.abs, entry.name);
|
|
886
|
-
const relFromRoot = path.relative(absRoot, entryAbs).replace(/\\/g, '/');
|
|
887
|
-
if (matchesAny(relFromRoot, excludeMatchers)) {
|
|
888
|
-
complete = false;
|
|
889
|
-
continue;
|
|
890
|
-
}
|
|
891
|
-
const virtualPath = current.virtual === '/'
|
|
892
|
-
? `/${entry.name}`
|
|
893
|
-
: `${current.virtual.replace(/\/+$/, '')}/${entry.name}`;
|
|
894
|
-
const isDir = entry.isDirectory();
|
|
895
|
-
let size;
|
|
896
|
-
let modifiedAt;
|
|
897
|
-
if (!isDir) {
|
|
898
|
-
try {
|
|
899
|
-
const stat = await fs.stat(entryAbs);
|
|
900
|
-
size = stat.size;
|
|
901
|
-
modifiedAt = stat.mtime.toISOString();
|
|
902
|
-
}
|
|
903
|
-
catch {
|
|
904
|
-
complete = false; /* transient stat failure — emit without metadata */
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
items.push({
|
|
908
|
-
uri: this.#toUri(virtualPath),
|
|
909
|
-
name: entry.name,
|
|
910
|
-
isDirectory: isDir,
|
|
911
|
-
...(size != null ? { size } : {}),
|
|
912
|
-
...(modifiedAt ? { modifiedAt } : {}),
|
|
913
|
-
});
|
|
914
|
-
if (isDir)
|
|
915
|
-
queue.push({ abs: entryAbs, virtual: virtualPath });
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
return { items, complete: complete && !truncated, truncated };
|
|
919
|
-
}
|
|
920
|
-
async count(rootUri, opts) {
|
|
921
|
-
// No default excludes — physical count must match walk() cardinality.
|
|
922
|
-
const excludes = opts?.excludeGlobs ?? [];
|
|
923
|
-
const cacheKey = this.#countCacheKey(rootUri, opts);
|
|
924
|
-
const cached = this.#countCache.get(cacheKey);
|
|
925
|
-
const now = Date.now();
|
|
926
|
-
if (cached && now - cached.ts < getBrainConfigNumber('brainCountCacheTtlMs', undefined, COUNT_CACHE_TTL_MS))
|
|
927
|
-
return cached.value;
|
|
928
|
-
const rootPath = this.#extractPath(rootUri) || '/';
|
|
929
|
-
let absRoot;
|
|
930
|
-
try {
|
|
931
|
-
absRoot = await resolveAndGuard(this.#root, rootPath === '/' ? '.' : rootPath);
|
|
932
|
-
}
|
|
933
|
-
catch (error) {
|
|
934
|
-
if (isSkippableFsError(error))
|
|
935
|
-
return 0;
|
|
936
|
-
throw error;
|
|
937
|
-
}
|
|
938
|
-
const excludeMatchers = compileGlobMatchers(excludes);
|
|
939
|
-
const cap = typeof opts?.maxScanFiles === 'number' && opts.maxScanFiles > 0
|
|
940
|
-
? opts.maxScanFiles
|
|
941
|
-
: DEFAULT_MAX_SCAN_FILES;
|
|
942
|
-
let total = 0;
|
|
943
|
-
const stack = [absRoot];
|
|
944
|
-
while (stack.length > 0) {
|
|
945
|
-
if (total >= cap)
|
|
946
|
-
break;
|
|
947
|
-
const current = stack.pop();
|
|
948
|
-
let entries;
|
|
949
|
-
try {
|
|
950
|
-
entries = await fs.readdir(current, { withFileTypes: true });
|
|
951
|
-
}
|
|
952
|
-
catch (error) {
|
|
953
|
-
if (isSkippableFsError(error))
|
|
954
|
-
continue;
|
|
955
|
-
throw error;
|
|
956
|
-
}
|
|
957
|
-
for (const entry of entries) {
|
|
958
|
-
if (entry.isSymbolicLink?.())
|
|
959
|
-
continue;
|
|
960
|
-
const entryAbs = path.join(current, entry.name);
|
|
961
|
-
const relFromRoot = path.relative(absRoot, entryAbs);
|
|
962
|
-
if (matchesAny(relFromRoot, excludeMatchers))
|
|
963
|
-
continue;
|
|
964
|
-
if (entry.isDirectory()) {
|
|
965
|
-
stack.push(entryAbs);
|
|
966
|
-
}
|
|
967
|
-
else if (entry.isFile()) {
|
|
968
|
-
total += 1;
|
|
969
|
-
if (total >= cap)
|
|
970
|
-
break;
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
this.#countCache.set(cacheKey, { value: total, ts: now });
|
|
975
|
-
return total;
|
|
976
|
-
}
|
|
977
|
-
async scanContent(rootUri, query, opts) {
|
|
978
|
-
if (!query || query.length === 0)
|
|
979
|
-
return [];
|
|
980
|
-
const rootPath = this.#extractPath(rootUri) || '/';
|
|
981
|
-
let absRoot;
|
|
982
|
-
try {
|
|
983
|
-
absRoot = await resolveAndGuard(this.#root, rootPath === '/' ? '.' : rootPath);
|
|
984
|
-
}
|
|
985
|
-
catch (error) {
|
|
986
|
-
if (isSkippableFsError(error))
|
|
987
|
-
return [];
|
|
988
|
-
throw error;
|
|
989
|
-
}
|
|
990
|
-
const bins = await resolveSearchBinaries();
|
|
991
|
-
const caseSensitive = opts?.caseSensitive === true;
|
|
992
|
-
const maxMatches = typeof opts?.maxMatches === 'number' && opts.maxMatches > 0 ? opts.maxMatches : 500;
|
|
993
|
-
let hasGit = false;
|
|
994
|
-
try {
|
|
995
|
-
const s = await fs.stat(path.join(absRoot, '.git'));
|
|
996
|
-
hasGit = s.isDirectory();
|
|
997
|
-
}
|
|
998
|
-
catch { /* no .git — fall through */ }
|
|
999
|
-
const excludeArgs = [];
|
|
1000
|
-
for (const pattern of opts?.excludeGlobs ?? []) {
|
|
1001
|
-
excludeArgs.push('--exclude-dir', pattern.replace(/\/\*\*$/, '').replace(/^\*\*\//, ''));
|
|
1002
|
-
}
|
|
1003
|
-
// The tool contract (tools/fs_search.json) promises LITERAL fixed-string
|
|
1004
|
-
// grep semantics — every engine branch passes -F so a query with regex
|
|
1005
|
-
// metacharacters (`${`, `[slug]`) neither hard-fails nor varies by which
|
|
1006
|
-
// binary wins the ladder. Fuzzy needs are semantic mode's job.
|
|
1007
|
-
if (hasGit && bins.gitGrep) {
|
|
1008
|
-
const args = ['grep', '-n', '--column', '--no-color', '-F'];
|
|
1009
|
-
if (!caseSensitive)
|
|
1010
|
-
args.push('-i');
|
|
1011
|
-
args.push('-e', query, '--');
|
|
1012
|
-
// Include/exclude ride the pathspec tail. Git pathspecs are
|
|
1013
|
-
// case-SENSITIVE by default (`:(icase)` restores parity with -i), and
|
|
1014
|
-
// plain pathspec `*` crosses `/` (wildmatch without pathname mode) —
|
|
1015
|
-
// semantics differ slightly from rg's gitignore-style globs; documented
|
|
1016
|
-
// best-effort. Before this, the git-grep branch (the primary branch for
|
|
1017
|
-
// repo checkouts) silently ignored every glob option.
|
|
1018
|
-
for (const pat of opts?.includeGlobs ?? []) {
|
|
1019
|
-
args.push(caseSensitive ? pat : `:(icase)${pat}`);
|
|
1020
|
-
}
|
|
1021
|
-
for (const pat of opts?.excludeGlobs ?? []) {
|
|
1022
|
-
args.push(`:(exclude)${pat}`);
|
|
1023
|
-
}
|
|
1024
|
-
return this.#parseGrepOutput(rootUri, absRoot, await runBinary(bins.gitGrep, args, absRoot, maxMatches), maxMatches);
|
|
1025
|
-
}
|
|
1026
|
-
if (bins.rg) {
|
|
1027
|
-
const args = ['-n', '--column', '--no-heading', '--color=never', '-F'];
|
|
1028
|
-
if (!caseSensitive)
|
|
1029
|
-
args.push('-i');
|
|
1030
|
-
for (const pat of opts?.includeGlobs ?? [])
|
|
1031
|
-
args.push('--glob', pat);
|
|
1032
|
-
for (const pat of opts?.excludeGlobs ?? [])
|
|
1033
|
-
args.push('--glob', `!${pat}`);
|
|
1034
|
-
args.push('-e', query, absRoot);
|
|
1035
|
-
return this.#parseGrepOutput(rootUri, absRoot, await runBinary(bins.rg, args, absRoot, maxMatches), maxMatches);
|
|
1036
|
-
}
|
|
1037
|
-
if (bins.grep) {
|
|
1038
|
-
const args = ['-R', '-n', '--color=never', '-F'];
|
|
1039
|
-
if (!caseSensitive)
|
|
1040
|
-
args.push('-i');
|
|
1041
|
-
// GNU grep --include matches the BASENAME only — the narrowest of the
|
|
1042
|
-
// three glob dialects; documented best-effort.
|
|
1043
|
-
for (const pat of opts?.includeGlobs ?? [])
|
|
1044
|
-
args.push(`--include=${pat}`);
|
|
1045
|
-
args.push(...excludeArgs, '-e', query, absRoot);
|
|
1046
|
-
return this.#parseGrepOutput(rootUri, absRoot, await runBinary(bins.grep, args, absRoot, maxMatches), maxMatches);
|
|
1047
|
-
}
|
|
1048
|
-
throw new Error('No text-search binary available on the host (tried rg, git grep, grep)');
|
|
1049
|
-
}
|
|
1050
|
-
#parseGrepOutput(rootUri, absRoot, output, maxMatches) {
|
|
1051
|
-
const lines = output.split('\n');
|
|
1052
|
-
const results = [];
|
|
1053
|
-
const isSourceRoot = rootUri.endsWith('://');
|
|
1054
|
-
const joinBase = isSourceRoot ? rootUri : rootUri.replace(/\/+$/, '');
|
|
1055
|
-
for (const line of lines) {
|
|
1056
|
-
if (results.length >= maxMatches)
|
|
1057
|
-
break;
|
|
1058
|
-
if (!line)
|
|
1059
|
-
continue;
|
|
1060
|
-
const m = line.match(/^(.+?):(\d+)(?::(\d+))?:(.*)$/);
|
|
1061
|
-
if (!m)
|
|
1062
|
-
continue;
|
|
1063
|
-
const filePath = m[1];
|
|
1064
|
-
const lineNo = parseInt(m[2], 10);
|
|
1065
|
-
const col = m[3] ? parseInt(m[3], 10) : undefined;
|
|
1066
|
-
const preview = m[4];
|
|
1067
|
-
const rel = path.relative(absRoot, filePath).replace(/\\/g, '/');
|
|
1068
|
-
const hasRel = rel && rel !== '.' && !rel.startsWith('..');
|
|
1069
|
-
const uri = !hasRel
|
|
1070
|
-
? (isSourceRoot ? rootUri : joinBase)
|
|
1071
|
-
: isSourceRoot ? `${joinBase}${rel}` : `${joinBase}/${rel}`;
|
|
1072
|
-
results.push({
|
|
1073
|
-
uri,
|
|
1074
|
-
line: lineNo,
|
|
1075
|
-
...(typeof col === 'number' && Number.isFinite(col) ? { column: col } : {}),
|
|
1076
|
-
preview,
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
return results;
|
|
1080
|
-
}
|
|
1081
|
-
async #walkFiles(rootPath, signal, excludeGlobs, maxScanFiles) {
|
|
1082
|
-
const rootAbs = await resolveAndGuard(this.#root, rootPath === '/' ? '.' : rootPath);
|
|
1083
|
-
const stack = [{ abs: rootAbs, virtual: rootPath === '/' ? '/' : rootPath }];
|
|
1084
|
-
const out = [];
|
|
1085
|
-
const cap = typeof maxScanFiles === 'number' && maxScanFiles > 0 ? maxScanFiles : DEFAULT_MAX_SCAN_FILES;
|
|
1086
|
-
// Prune excluded trees DURING the walk — never descend into node_modules /
|
|
1087
|
-
// .git / dist etc. Without this a repo-sized source blows the MAX_SCAN_FILES
|
|
1088
|
-
// cap on excluded files and starves alphabetically-late included trees
|
|
1089
|
-
// (e.g. `.claude`, which sorts first → pushed first → popped LAST in DFS).
|
|
1090
|
-
// A directory is pruned when an exclude matches `<rel>/` (so `node_modules/**`
|
|
1091
|
-
// catches the bare dir); files match their plain relative path.
|
|
1092
|
-
const excludeRegexes = compileGlobPatterns(excludeGlobs ?? []);
|
|
1093
|
-
const relOf = (virtual) => (virtual.startsWith('/') ? virtual.slice(1) : virtual);
|
|
1094
|
-
const isExcludedDir = (virtual) => {
|
|
1095
|
-
const rel = relOf(virtual);
|
|
1096
|
-
return rel.length > 0 && excludeRegexes.some((rx) => rx.test(`${rel}/`));
|
|
1097
|
-
};
|
|
1098
|
-
const isExcludedFile = (virtual) => {
|
|
1099
|
-
const rel = relOf(virtual);
|
|
1100
|
-
return rel.length > 0 && excludeRegexes.some((rx) => rx.test(rel));
|
|
1101
|
-
};
|
|
1102
|
-
while (stack.length > 0) {
|
|
1103
|
-
// Cooperative cancellation per directory pop — a repo-sized walk
|
|
1104
|
-
// (~200k file cap) must stop within one directory of an abort.
|
|
1105
|
-
signal?.throwIfAborted();
|
|
1106
|
-
if (out.length >= cap)
|
|
1107
|
-
break;
|
|
1108
|
-
const current = stack.pop();
|
|
1109
|
-
if (!current)
|
|
1110
|
-
continue;
|
|
1111
|
-
let entries;
|
|
1112
|
-
try {
|
|
1113
|
-
entries = await fs.readdir(current.abs, { withFileTypes: true });
|
|
1114
|
-
}
|
|
1115
|
-
catch (error) {
|
|
1116
|
-
if (isSkippableFsError(error))
|
|
1117
|
-
continue;
|
|
1118
|
-
throw error;
|
|
1119
|
-
}
|
|
1120
|
-
for (const entry of entries) {
|
|
1121
|
-
if (out.length >= cap)
|
|
1122
|
-
break;
|
|
1123
|
-
if (entry.isSymbolicLink?.())
|
|
1124
|
-
continue;
|
|
1125
|
-
const entryAbs = path.join(current.abs, entry.name);
|
|
1126
|
-
const entryVirtual = path.join(current.virtual === '/' ? '/' : current.virtual, entry.name);
|
|
1127
|
-
if (entry.isDirectory()) {
|
|
1128
|
-
if (!isExcludedDir(entryVirtual))
|
|
1129
|
-
stack.push({ abs: entryAbs, virtual: entryVirtual });
|
|
1130
|
-
continue;
|
|
1131
|
-
}
|
|
1132
|
-
if (isExcludedFile(entryVirtual))
|
|
1133
|
-
continue;
|
|
1134
|
-
try {
|
|
1135
|
-
const stat = await fs.stat(entryAbs);
|
|
1136
|
-
out.push({
|
|
1137
|
-
uri: this.#toUri(entryVirtual),
|
|
1138
|
-
name: entry.name,
|
|
1139
|
-
size: stat.size,
|
|
1140
|
-
modifiedAt: stat.mtime.toISOString(),
|
|
1141
|
-
});
|
|
1142
|
-
}
|
|
1143
|
-
catch {
|
|
1144
|
-
/* transient IO errors skip the entry, not the sync */
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
out.sort((a, b) => a.uri.localeCompare(b.uri));
|
|
1149
|
-
return { items: out, truncated: out.length >= cap };
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
/**
|
|
1153
|
-
* Factory: builds a LocalDiskConnector instance for a specific source binding.
|
|
1154
|
-
*
|
|
1155
|
-
* Resolves `config.root` against the current projectRoot
|
|
1156
|
-
* (`resolveLocalRoot`): a relative path joins onto `ctx.projectRoot`, an
|
|
1157
|
-
* absolute one is the container path as written.
|
|
1158
|
-
*/
|
|
1159
|
-
export function createLocalDiskConnector(ctx) {
|
|
1160
|
-
const cfg = ctx.config;
|
|
1161
|
-
if (typeof cfg.root !== 'string' || cfg.root.length === 0) {
|
|
1162
|
-
throw new Error(`LocalDiskConnector: config.root is required for source "${ctx.source}".`);
|
|
1163
|
-
}
|
|
1164
|
-
const { resolvedRoot } = resolveLocalRoot({
|
|
1165
|
-
configured: cfg.root,
|
|
1166
|
-
projectRoot: ctx.projectRoot ?? '',
|
|
1167
|
-
source: ctx.source,
|
|
1168
|
-
});
|
|
1169
|
-
// Host-pair precedence: explicit config wins; then derive from the
|
|
1170
|
-
// `NEURALIS_MOUNT_*` os-mount table so a UI-added source whose root sits under a `pnpm
|
|
1171
|
-
// neuralis:mount`-created bind (e.g. `/mounts/repo`) resolves its host path
|
|
1172
|
-
// without the user having to hand-edit `hostRoot` into the config.
|
|
1173
|
-
const hostRoot = cfg.hostRoot ??
|
|
1174
|
-
deriveHostRootFromOsMounts(resolvedRoot, discoverOsMounts(process.env));
|
|
1175
|
-
return new LocalDiskConnector(resolvedRoot, ctx.source, hostRoot);
|
|
1176
|
-
}
|
|
1177
|
-
/**
|
|
1178
|
-
* Derive a host-side root for `resolvedRoot` from the os-mount table. Returns
|
|
1179
|
-
* the host path for the *longest* matching mount whose `containerPath` is the
|
|
1180
|
-
* root itself or a path-boundary-correct ancestor (`/mounts/repo` matches
|
|
1181
|
-
* `/mounts/repo` and `/mounts/repo/sub`, but NOT `/mounts/repo2`). Mounts
|
|
1182
|
-
* without a `hostPath` (no `NEURALIS_MOUNT_HOST_*` pair) are skipped. Returns
|
|
1183
|
-
* `undefined` when nothing matches, so the caller falls through to the
|
|
1184
|
-
* fail-loud `OsUriUnresolvableError` path in Docker.
|
|
1185
|
-
*/
|
|
1186
|
-
export function deriveHostRootFromOsMounts(resolvedRoot, mounts) {
|
|
1187
|
-
const target = path.resolve(resolvedRoot);
|
|
1188
|
-
let best;
|
|
1189
|
-
for (const mount of mounts) {
|
|
1190
|
-
if (!mount.hostPath)
|
|
1191
|
-
continue;
|
|
1192
|
-
const container = path.resolve(mount.containerPath);
|
|
1193
|
-
const isMatch = target === container || target.startsWith(`${container}${path.sep}`);
|
|
1194
|
-
if (!isMatch)
|
|
1195
|
-
continue;
|
|
1196
|
-
if (!best || container.length > best.containerPath.length) {
|
|
1197
|
-
const tail = target.slice(container.length); // '' when target === container
|
|
1198
|
-
best = { containerPath: container, hostRoot: path.join(path.resolve(mount.hostPath), tail) };
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
return best?.hostRoot;
|
|
1202
|
-
}
|
|
1203
|
-
/**
|
|
1204
|
-
* Canonical container-root resolution for a local source's configured root.
|
|
1205
|
-
* Exported so the brain-core API can hand agent-core's shell uri-policy gate
|
|
1206
|
-
* the SAME resolved roots the connector itself mounts — the two branches
|
|
1207
|
-
* (relative under the project root, absolute as written) are never re-typed.
|
|
1208
|
-
*/
|
|
1209
|
-
export function resolveLocalRoot(params) {
|
|
1210
|
-
const { configured, projectRoot } = params;
|
|
1211
|
-
if (!path.isAbsolute(configured)) {
|
|
1212
|
-
return { resolvedRoot: projectRoot ? path.join(projectRoot, configured) : configured };
|
|
1213
|
-
}
|
|
1214
|
-
// An absolute root is a container path as written — never re-targeted by a
|
|
1215
|
-
// segment of it that happens to spell the project id.
|
|
1216
|
-
return { resolvedRoot: path.resolve(configured) };
|
|
1217
|
-
}
|
|
1218
|
-
function detectDockerRuntime() {
|
|
1219
|
-
const flag = process.env.NEURALIS_DOCKER;
|
|
1220
|
-
if (flag === 'true' || flag === '1')
|
|
1221
|
-
return true;
|
|
1222
|
-
try {
|
|
1223
|
-
if (existsSync('/.dockerenv'))
|
|
1224
|
-
return true;
|
|
1225
|
-
if (existsSync('/run/.containerenv'))
|
|
1226
|
-
return true;
|
|
1227
|
-
}
|
|
1228
|
-
catch {
|
|
1229
|
-
/* no fs access — assume non-docker */
|
|
1230
|
-
}
|
|
1231
|
-
return false;
|
|
1232
|
-
}
|
|
1233
|
-
//# sourceMappingURL=LocalDiskConnector.js.map
|