@neuralis/brain-core 0.3.2 → 0.4.1
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 +80 -1410
- package/app/README.md +1 -1
- 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 +17 -2
- package/app/filesystem/api/README.md +3 -3
- package/app/filesystem/api/index.ts +35 -11
- package/app/filesystem/api/projectPackages.ts +13 -102
- package/app/filesystem/domain/types.ts +30 -10
- package/app/filesystem/hooks/README.md +3 -3
- package/app/filesystem/hooks/useDiscoverableSources.ts +19 -30
- package/app/filesystem/hooks/useFilesystemSSE.ts +1 -1
- package/app/filesystem/host/README.md +5 -3
- package/app/filesystem/host/buildFilesystemHostValue.ts +19 -0
- package/app/filesystem/host/installFilesystemHostComponents.tsx +1 -1
- package/app/filesystem/lib/sourceKindForm.ts +118 -39
- package/app/filesystem/lib/treeVisualState.tsx +98 -4
- package/app/filesystem/store/README.md +1 -0
- package/app/filesystem/store/filesystemStore.ts +86 -5
- 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/FileEditor.tsx +15 -10
- package/app/filesystem/ui/FileTree.tsx +41 -16
- package/app/filesystem/ui/FolderPicker.tsx +251 -0
- package/app/filesystem/ui/GitControlPanel.tsx +5 -0
- package/app/filesystem/ui/PackagesPanel.tsx +52 -374
- package/app/filesystem/ui/PolicyEditor.tsx +109 -24
- package/app/filesystem/ui/README.md +17 -11
- package/app/filesystem/ui/RawMediaPreview.tsx +58 -0
- package/app/filesystem/ui/SourceBranchHeader.tsx +4 -4
- package/app/filesystem/ui/SourcesPanel.tsx +420 -360
- package/app/filesystem/ui/Toolbar.tsx +1 -2
- package/app/shared/filesystem.png +0 -0
- package/dist/app/host.js +53 -48
- 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/config/env.d.ts +8 -0
- package/dist/src/config/env.d.ts.map +1 -1
- package/dist/src/config/env.js +17 -6
- package/dist/src/config/env.js.map +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/BrainError.d.ts +14 -0
- package/dist/src/domain/BrainError.d.ts.map +1 -1
- package/dist/src/domain/BrainError.js +21 -0
- package/dist/src/domain/BrainError.js.map +1 -1
- 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 +8 -16
- package/dist/src/domain/mime.d.ts.map +1 -1
- package/dist/src/domain/mime.js +10 -99
- 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/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 +29 -1
- 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 +2 -3
- package/dist/src/embeddings/models.d.ts.map +1 -1
- package/dist/src/embeddings/models.js +1 -2
- package/dist/src/embeddings/models.js.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/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 +31 -19
- package/dist/src/git/sourceGitControl.d.ts.map +1 -1
- package/dist/src/git/sourceGitControl.js +208 -92
- 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 +12 -16
- 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.map +1 -1
- package/dist/src/routes/approve.js +3 -3
- package/dist/src/routes/approve.js.map +1 -1
- package/dist/src/routes/git.d.ts.map +1 -1
- package/dist/src/routes/git.js +183 -54
- package/dist/src/routes/git.js.map +1 -1
- package/dist/src/routes/health.d.ts.map +1 -1
- package/dist/src/routes/health.js +8 -5
- package/dist/src/routes/health.js.map +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 +20 -4
- package/dist/src/routes/helpers.d.ts.map +1 -1
- package/dist/src/routes/helpers.js +38 -5
- package/dist/src/routes/helpers.js.map +1 -1
- package/dist/src/routes/list.d.ts.map +1 -1
- package/dist/src/routes/list.js +5 -6
- package/dist/src/routes/list.js.map +1 -1
- package/dist/src/routes/pending.d.ts.map +1 -1
- package/dist/src/routes/pending.js +5 -5
- package/dist/src/routes/pending.js.map +1 -1
- package/dist/src/routes/policies.d.ts +6 -4
- package/dist/src/routes/policies.d.ts.map +1 -1
- package/dist/src/routes/policies.js +22 -16
- package/dist/src/routes/policies.js.map +1 -1
- package/dist/src/routes/raw.d.ts.map +1 -1
- package/dist/src/routes/raw.js +68 -14
- package/dist/src/routes/raw.js.map +1 -1
- package/dist/src/routes/read.d.ts.map +1 -1
- package/dist/src/routes/read.js +3 -3
- package/dist/src/routes/read.js.map +1 -1
- package/dist/src/routes/search.d.ts.map +1 -1
- package/dist/src/routes/search.js +3 -3
- package/dist/src/routes/search.js.map +1 -1
- package/dist/src/routes/sources.d.ts +60 -18
- package/dist/src/routes/sources.d.ts.map +1 -1
- package/dist/src/routes/sources.js +420 -308
- package/dist/src/routes/sources.js.map +1 -1
- package/dist/src/routes/upload.d.ts.map +1 -1
- package/dist/src/routes/upload.js +3 -3
- package/dist/src/routes/upload.js.map +1 -1
- package/dist/src/services/BridgeService.d.ts +15 -35
- package/dist/src/services/BridgeService.d.ts.map +1 -1
- package/dist/src/services/BridgeService.js +30 -88
- package/dist/src/services/BridgeService.js.map +1 -1
- package/dist/src/services/FilesystemService.d.ts +3 -2
- package/dist/src/services/FilesystemService.d.ts.map +1 -1
- package/dist/src/services/FilesystemService.js +116 -32
- 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 +71 -27
- 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 +45 -10
- 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 +125 -42
- package/dist/src/services/declaredSources.d.ts.map +1 -1
- package/dist/src/services/declaredSources.js +117 -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 +14 -2
- package/dist/src/sync/SyncJobQueue.d.ts.map +1 -1
- package/dist/src/sync/SyncJobQueue.js +56 -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 +6 -0
- package/dist/src/tools/execCtx.d.ts.map +1 -1
- package/dist/src/tools/execCtx.js +11 -0
- package/dist/src/tools/execCtx.js.map +1 -1
- package/dist/src/tools/fs_list.js +2 -2
- package/dist/src/tools/fs_list.js.map +1 -1
- package/dist/src/tools/fs_read.js +2 -2
- package/dist/src/tools/fs_read.js.map +1 -1
- package/dist/src/tools/fs_search.js +2 -2
- package/dist/src/tools/fs_search.js.map +1 -1
- package/dist/src/tools/fs_write.js +2 -2
- package/dist/src/tools/fs_write.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/deprovisionProject.d.ts +4 -5
- package/dist/src/usecases/deprovisionProject.d.ts.map +1 -1
- package/dist/src/usecases/deprovisionProject.js +3 -8
- package/dist/src/usecases/deprovisionProject.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 +95 -79
- 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 +17 -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/searchFiles.d.ts.map +1 -1
- package/dist/src/usecases/searchFiles.js +13 -3
- package/dist/src/usecases/searchFiles.js.map +1 -1
- package/dist/src/usecases/sourcePackageInventory.d.ts +10 -2
- package/dist/src/usecases/sourcePackageInventory.d.ts.map +1 -1
- package/dist/src/usecases/sourcePackageInventory.js +24 -7
- package/dist/src/usecases/sourcePackageInventory.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/vectorActivityWatch.d.ts +20 -13
- package/dist/src/usecases/vectorActivityWatch.d.ts.map +1 -1
- package/dist/src/usecases/vectorActivityWatch.js +61 -36
- package/dist/src/usecases/vectorActivityWatch.js.map +1 -1
- package/dist/src/usecases/vectorMaintenance.d.ts.map +1 -1
- package/dist/src/usecases/vectorMaintenance.js +16 -1
- package/dist/src/usecases/vectorMaintenance.js.map +1 -1
- package/dist/src/usecases/write/index.d.ts.map +1 -1
- package/dist/src/usecases/write/index.js +17 -2
- package/dist/src/usecases/write/index.js.map +1 -1
- package/dist/src/usecases/write/occupancy.d.ts +5 -1
- package/dist/src/usecases/write/occupancy.d.ts.map +1 -1
- package/dist/src/usecases/write/occupancy.js +13 -4
- package/dist/src/usecases/write/occupancy.js.map +1 -1
- package/dist/src/usecases/write/structure.d.ts.map +1 -1
- package/dist/src/usecases/write/structure.js +20 -5
- package/dist/src/usecases/write/structure.js.map +1 -1
- package/dist/src/vector/FilesystemVectorIndex.d.ts +72 -2
- package/dist/src/vector/FilesystemVectorIndex.d.ts.map +1 -1
- package/dist/src/vector/FilesystemVectorIndex.js +211 -27
- package/dist/src/vector/FilesystemVectorIndex.js.map +1 -1
- package/dist/src/vector/InMemoryVectorIndex.d.ts.map +1 -1
- package/dist/src/vector/InMemoryVectorIndex.js +55 -7
- package/dist/src/vector/InMemoryVectorIndex.js.map +1 -1
- package/dist/src/vector/qdrantClient.d.ts +18 -1
- package/dist/src/vector/qdrantClient.d.ts.map +1 -1
- package/dist/src/vector/qdrantClient.js +48 -6
- package/dist/src/vector/qdrantClient.js.map +1 -1
- package/dist/src/wiring.d.ts +10 -12
- package/dist/src/wiring.d.ts.map +1 -1
- package/dist/src/wiring.js +101 -61
- package/dist/src/wiring.js.map +1 -1
- package/package.json +166 -54
- package/skills/connect-business-data/SKILL.md +2 -2
- package/skills/create-package/SKILL.md +18 -18
- package/skills/create-package/references/config-and-credentials.md +16 -13
- package/skills/create-package/references/connectors-and-uri-policies.md +36 -27
- package/skills/create-package/references/manifest-and-discovery.md +12 -12
- package/skills/create-package/references/runtime-and-build.md +18 -18
- package/skills/create-package/references/skills-and-context.md +35 -19
- 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 +17 -17
- package/skills/create-package/references/tools-and-schemas.md +5 -5
- package/skills/create-package/scripts/{check-package.sh → check-package.mjs} +78 -64
- package/skills/create-package/scripts/{verify-loaded.sh → verify-loaded.mjs} +156 -162
- package/skills/git/SKILL.md +31 -29
- package/skills/manage-files/SKILL.md +11 -14
- package/skills/manage-sources/SKILL.md +85 -45
- package/skills/manage-uri-policy/SKILL.md +9 -6
- package/skills/sync/SKILL.md +2 -2
- 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/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
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
/**
|
|
3
|
+
* The `host` connector — ONE class for a folder as a source, wherever the
|
|
4
|
+
* folder lives. A transport (`HostFsTransport`, the kernel port) moves the bytes
|
|
5
|
+
* and runs the kernel's one folder walk on the machine that holds them:
|
|
6
|
+
*
|
|
7
|
+
* - no `device` in the stored config ⇒ `local`, this process's own filesystem
|
|
8
|
+
* (`InProcessTransport`): a Docker mount, the native project zone, `data`,
|
|
9
|
+
* `packages`, the App Zone;
|
|
10
|
+
* - `device` ⇒ that paired device's broker over its link — agent-core's
|
|
11
|
+
* transport, handed over through its device API (`devices.openTransport`), so
|
|
12
|
+
* no broker code lives here.
|
|
13
|
+
*
|
|
14
|
+
* Everything a source MEANS is decided here, once: uri↔path in the folder's
|
|
15
|
+
* own flavour, the MIME table, the inline caps a model read honours, the
|
|
16
|
+
* byte-exact read, the bounded count cache, the watch-to-uri mapping and the
|
|
17
|
+
* package reader. The kernel's `sourcePlane` reads the same stored field that
|
|
18
|
+
* picks the transport.
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'node:fs/promises';
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { posix as posixPath, win32 as win32Path } from 'node:path';
|
|
24
|
+
import { createHash } from 'node:crypto';
|
|
25
|
+
import { execFile } from 'node:child_process';
|
|
26
|
+
import { CONNECTOR_PORT_VERSION, ConnectorRawReadTooLarge, ConnectorRootAccessError, HOST_SOURCE_KIND, isPackageFolderPath, sourcePlane, } from '@neuralis/package-system/contracts';
|
|
27
|
+
import { detectDocker, discoverOsMounts, isPathInside } from '@neuralis/package-system/paths';
|
|
28
|
+
import { isSkippableFsError } from '@neuralis/package-system/data';
|
|
29
|
+
import { binaryMimeTypeForPath, inferMimeTypeFromFilename } from '../domain/mime.js';
|
|
30
|
+
import { looksBinaryContent } from '../domain/text.js';
|
|
31
|
+
import { getBrainConfigBoolean, getBrainConfigNumber, getInlineAudioCapBytes, getInlineImageCapBytes, getInlinePdfCapBytes, getInlineVideoCapBytes, } from '../config/env.js';
|
|
32
|
+
import { DEFAULT_MAX_SCAN_FILES } from '../domain/scanCap.js';
|
|
33
|
+
import { CHILD_ENV, InProcessTransport, OsUriUnresolvableError, deriveHostRootFromOsMounts, resolveLocalRoot, } from './inProcessTransport.js';
|
|
34
|
+
/**
|
|
35
|
+
* The capability set of the `host` kind. It MUST equal the `host` connector's
|
|
36
|
+
* `capabilities` array in `package.json#neuralis.connectors` — the loader binds
|
|
37
|
+
* one kind to one set and refuses a drifted instance (boot-fatal for a native
|
|
38
|
+
* connector). `hostCapabilities.test.ts` derives the expectation from the manifest.
|
|
39
|
+
*/
|
|
40
|
+
export const HOST_CAPABILITIES = [
|
|
41
|
+
'list', 'read', 'stat', 'write', 'delete', 'move', 'mkdir', 'rmdir',
|
|
42
|
+
'scanDelta', 'walk', 'count', 'peekCount', 'scanContent', 'subscribeEvents', 'resolveOsUri',
|
|
43
|
+
'rawRead',
|
|
44
|
+
];
|
|
45
|
+
/** How long a `count` is fresh for `peekCount` and `count` (`brainCountCacheTtlMs`); older, `count` serves it while one walk refreshes it. */
|
|
46
|
+
export const COUNT_CACHE_TTL_MS = 60_000;
|
|
47
|
+
/** Count-cache entries one source keeps (each a key string + two numbers), fresh or stale; past it the oldest goes. */
|
|
48
|
+
export const MAX_COUNT_CACHE_ENTRIES = 256;
|
|
49
|
+
/** The folder budget of ONE package folder's watch (a device's broker clamps above). */
|
|
50
|
+
export const PACKAGE_FOLDER_WATCH_MAX_FOLDERS = 5_000;
|
|
51
|
+
/** Trees a package watch never enters — they hold no contribution the loader reads. */
|
|
52
|
+
const PACKAGE_WATCH_EXCLUDES = ['**/node_modules/**', '**/.git/**'];
|
|
53
|
+
/** A device package file is read whole only under this many bytes. */
|
|
54
|
+
const PACKAGE_FILE_MAX_BYTES = 1_000_000;
|
|
55
|
+
/** One write RPC/syscall carries at most this many bytes; a larger file goes in chunks. */
|
|
56
|
+
export const WRITE_CHUNK_BYTES = 3 * 1024 * 1024;
|
|
57
|
+
/** The ONE byte ceiling of a whole-file read (`brainMaxReadFileMb`, default 100 MB) — the same on both transports, read LIVE per call. */
|
|
58
|
+
const maxReadBytes = () => getBrainConfigNumber('brainMaxReadFileMb', undefined, 100) * 1024 * 1024;
|
|
59
|
+
/** PDF text-extract cap (`brainPdfTextExcerptBytes`, default 200 KB), LIVE. */
|
|
60
|
+
const pdfTextExcerptBytes = () => getBrainConfigNumber('brainPdfTextExcerptBytes', undefined, 200 * 1024);
|
|
61
|
+
/** First-page render, opt-in (`fsReadPdfRender`), LIVE. */
|
|
62
|
+
const pdfRenderFirstPage = () => getBrainConfigBoolean('fsReadPdfRender', 'NEURALIS_FS_READ_PDF_RENDER', false);
|
|
63
|
+
function formatBytes(n) {
|
|
64
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
65
|
+
return '0 B';
|
|
66
|
+
if (n < 1024)
|
|
67
|
+
return `${n} B`;
|
|
68
|
+
if (n < 1024 * 1024)
|
|
69
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
70
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
71
|
+
}
|
|
72
|
+
/** The per-MIME inline cap a model read honours; `null` ⇒ the MIME is never inlined. */
|
|
73
|
+
function inlineCapForMime(mimeType) {
|
|
74
|
+
if (mimeType === 'application/pdf')
|
|
75
|
+
return getInlinePdfCapBytes();
|
|
76
|
+
if (mimeType.startsWith('image/'))
|
|
77
|
+
return getInlineImageCapBytes();
|
|
78
|
+
if (mimeType.startsWith('video/'))
|
|
79
|
+
return getInlineVideoCapBytes();
|
|
80
|
+
if (mimeType.startsWith('audio/'))
|
|
81
|
+
return getInlineAudioCapBytes();
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
let pdftotextProbed = null;
|
|
85
|
+
let pdftoppmProbed = null;
|
|
86
|
+
function whichBinary(name) {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
const child = execFile('which', [name], { env: CHILD_ENV }, (err, stdout) => {
|
|
89
|
+
if (err)
|
|
90
|
+
return resolve(false);
|
|
91
|
+
resolve(typeof stdout === 'string' && stdout.trim().length > 0);
|
|
92
|
+
});
|
|
93
|
+
child.on('error', () => resolve(false));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async function hasPdftotext() {
|
|
97
|
+
if (pdftotextProbed === null)
|
|
98
|
+
pdftotextProbed = await whichBinary('pdftotext');
|
|
99
|
+
return pdftotextProbed;
|
|
100
|
+
}
|
|
101
|
+
async function hasPdftoppm() {
|
|
102
|
+
if (pdftoppmProbed === null)
|
|
103
|
+
pdftoppmProbed = await whichBinary('pdftoppm');
|
|
104
|
+
return pdftoppmProbed;
|
|
105
|
+
}
|
|
106
|
+
async function pdftotextExcerpt(absPath) {
|
|
107
|
+
const excerptBytes = pdfTextExcerptBytes();
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
const child = execFile('pdftotext', [absPath, '-'], { timeout: 10_000, maxBuffer: excerptBytes * 4, env: CHILD_ENV }, (err, stdout) => {
|
|
110
|
+
if (err)
|
|
111
|
+
return resolve(null);
|
|
112
|
+
const out = String(stdout ?? '');
|
|
113
|
+
resolve(out.length > excerptBytes ? `${out.slice(0, excerptBytes)}\n\n…(pdftotext output truncated at ${excerptBytes} bytes)` : out);
|
|
114
|
+
});
|
|
115
|
+
child.on('error', () => resolve(null));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* THE PDF→PNG renderer (`pdftoppm -r 100`): pages `first..last`, in page order,
|
|
120
|
+
* one private temp directory per call. A failed or timed-out render answers `[]`.
|
|
121
|
+
*/
|
|
122
|
+
async function pdftoppmPages(absPath, first, last) {
|
|
123
|
+
const from = Math.max(1, Math.floor(first));
|
|
124
|
+
const to = Math.max(from, Math.floor(last));
|
|
125
|
+
let dir;
|
|
126
|
+
try {
|
|
127
|
+
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'neuralis-pdfrender-'));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const ok = await new Promise((resolve) => {
|
|
134
|
+
const child = execFile('pdftoppm', ['-f', String(from), '-l', String(to), '-r', '100', '-png', absPath, path.join(dir, 'page')], { timeout: 15_000 * (to - from + 1), env: CHILD_ENV }, (err) => resolve(!err));
|
|
135
|
+
child.on('error', () => resolve(false));
|
|
136
|
+
});
|
|
137
|
+
if (!ok)
|
|
138
|
+
return [];
|
|
139
|
+
const files = (await fs.readdir(dir))
|
|
140
|
+
.map((name) => ({ name, page: Number(/-(\d+)\.png$/.exec(name)?.[1]) }))
|
|
141
|
+
.filter((f) => Number.isFinite(f.page))
|
|
142
|
+
.sort((a, b) => a.page - b.page);
|
|
143
|
+
const pages = [];
|
|
144
|
+
for (const f of files)
|
|
145
|
+
pages.push({ page: f.page, png: await fs.readFile(path.join(dir, f.name)) });
|
|
146
|
+
return pages;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await fs.rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The source root itself is missing or unreadable — a listing of the ROOT
|
|
157
|
+
* throws this (the abstract base is what routes and services catch); a missing
|
|
158
|
+
* sub-folder lists as empty.
|
|
159
|
+
*/
|
|
160
|
+
export class HostRootAccessError extends ConnectorRootAccessError {
|
|
161
|
+
constructor(code, rootPath, uri, cause) {
|
|
162
|
+
super(code, rootPath, uri, code === 'ROOT_MISSING'
|
|
163
|
+
? `Source root "${rootPath}" does not exist`
|
|
164
|
+
: `Source root "${rootPath}" is not readable (permission denied)`, cause);
|
|
165
|
+
this.name = 'HostRootAccessError';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function rootAccessCode(error) {
|
|
169
|
+
const code = error?.code;
|
|
170
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
171
|
+
return 'ROOT_MISSING';
|
|
172
|
+
if (code === 'EACCES' || code === 'EPERM')
|
|
173
|
+
return 'ROOT_PERMISSION_DENIED';
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
/** A Windows drive-absolute root (`C:\…`), normalised, trailing separator dropped except on a drive root; `''` otherwise. */
|
|
177
|
+
function windowsRoot(raw) {
|
|
178
|
+
if (!/^[A-Za-z]:[\\/]/.test(raw))
|
|
179
|
+
return '';
|
|
180
|
+
const normal = win32Path.normalize(raw);
|
|
181
|
+
return normal === win32Path.parse(normal).root ? normal : normal.replace(/[\\/]+$/, '');
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The fixed code a transport refusal carries (`devices_disabled`,
|
|
185
|
+
* `device_offline`, `others_denied`, `device_owner_ungranted`, …) — never its
|
|
186
|
+
* text, which can name a device path. `undefined` for anything else (an errno,
|
|
187
|
+
* a plain failure): "could not ask" and "gone" stay apart.
|
|
188
|
+
*/
|
|
189
|
+
export function transportRefusalReason(error) {
|
|
190
|
+
const reason = error?.reason;
|
|
191
|
+
return typeof reason === 'string' && /^[a-z][a-z0-9_]{0,63}$/.test(reason) ? reason : undefined;
|
|
192
|
+
}
|
|
193
|
+
export class HostConnector {
|
|
194
|
+
kind = HOST_SOURCE_KIND;
|
|
195
|
+
source;
|
|
196
|
+
portVersion = CONNECTOR_PORT_VERSION;
|
|
197
|
+
#root;
|
|
198
|
+
#osRoot;
|
|
199
|
+
/** `#osRoot` normalized — the base a clean uri's `os_uri` is joined onto. */
|
|
200
|
+
#osBase;
|
|
201
|
+
#transport;
|
|
202
|
+
#path;
|
|
203
|
+
#capabilities = Object.freeze(new Set(HOST_CAPABILITIES));
|
|
204
|
+
/** A total (`value`) or a failed walk (`error`, and the last total if one existed) — each kept for the TTL. */
|
|
205
|
+
#countCache = new Map();
|
|
206
|
+
/** The ONE walk per count key in flight; removed when it settles, so bounded by the keys asked. */
|
|
207
|
+
#countWalks = new Map();
|
|
208
|
+
constructor(init) {
|
|
209
|
+
this.source = init.source;
|
|
210
|
+
this.#transport = init.transport;
|
|
211
|
+
this.#path = init.transport.flavour === 'win32' ? win32Path : posixPath;
|
|
212
|
+
this.#root = this.#path.normalize(init.root);
|
|
213
|
+
if (init.osRoot !== undefined) {
|
|
214
|
+
this.#osRoot = init.osRoot;
|
|
215
|
+
this.#osBase = this.#path.normalize(init.osRoot);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
capabilities() {
|
|
219
|
+
return this.#capabilities;
|
|
220
|
+
}
|
|
221
|
+
/** The paired device this folder lives on; `undefined` for an in-process folder. */
|
|
222
|
+
get device() {
|
|
223
|
+
return this.#transport.device;
|
|
224
|
+
}
|
|
225
|
+
/** The root on the folder's OWN machine (a device path for a device folder) — server-side only, never serialized. */
|
|
226
|
+
get root() {
|
|
227
|
+
return this.#root;
|
|
228
|
+
}
|
|
229
|
+
/** The root on THIS process's disk — `undefined` for a device folder (that disk is not ours). */
|
|
230
|
+
get localRoot() {
|
|
231
|
+
return this.#transport.kind === 'local' ? this.#root : undefined;
|
|
232
|
+
}
|
|
233
|
+
/** The transport this folder is reached through (the git engine's exec seam). */
|
|
234
|
+
get transport() {
|
|
235
|
+
return this.#transport;
|
|
236
|
+
}
|
|
237
|
+
/** The uri's path after `<source>://`, leading separators dropped; a foreign uri throws. */
|
|
238
|
+
#rest(uri) {
|
|
239
|
+
const prefix = `${this.source}://`;
|
|
240
|
+
if (!uri.startsWith(prefix))
|
|
241
|
+
throw new Error(`URI "${uri}" does not belong to source "${this.source}"`);
|
|
242
|
+
return uri.slice(prefix.length).replace(/^[\\/]+/, '');
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* `rest` as a path under `base` when it is lexically CLEAN — `/`-separated
|
|
246
|
+
* names, none empty, `.` or `..`, no `\` and no `:` — so the join cannot
|
|
247
|
+
* leave `base` and needs no normalize or containment pass (a listing
|
|
248
|
+
* resolves one per file). A name that only STARTS with dots (`..notes`,
|
|
249
|
+
* `...`) is clean, exactly as the kernel `isPathInside` reads it. `undefined`
|
|
250
|
+
* sends any other shape through the full normalize + `isPathInside` clamp.
|
|
251
|
+
*/
|
|
252
|
+
#under(base, rest) {
|
|
253
|
+
const trimmed = rest.replace(/\/+$/, '');
|
|
254
|
+
if (/[\\:]|(?:^|\/)\.{0,2}(?:\/|$)/.test(trimmed))
|
|
255
|
+
return trimmed === '' ? base : undefined;
|
|
256
|
+
const sep = this.#path.sep;
|
|
257
|
+
return `${base}${base.endsWith(sep) ? '' : sep}${sep === '/' ? trimmed : trimmed.replaceAll('/', sep)}`;
|
|
258
|
+
}
|
|
259
|
+
/** `<source>://a/b` → the absolute path on the folder's machine; `..` never climbs out. */
|
|
260
|
+
#abs(uri) {
|
|
261
|
+
const rest = this.#rest(uri);
|
|
262
|
+
const clean = this.#under(this.#root, rest);
|
|
263
|
+
if (clean !== undefined)
|
|
264
|
+
return clean;
|
|
265
|
+
const p = this.#path;
|
|
266
|
+
const joined = p.normalize(rest ? p.join(this.#root, rest) : this.#root);
|
|
267
|
+
const resolved = joined.length > this.#root.length ? joined.replace(/[\\/]+$/, '') : joined;
|
|
268
|
+
if (resolved !== this.#root && !isPathInside(this.#root, resolved, p)) {
|
|
269
|
+
throw new Error(`Path traversal blocked: "${uri}" escapes the source root`);
|
|
270
|
+
}
|
|
271
|
+
return resolved;
|
|
272
|
+
}
|
|
273
|
+
/** A root-relative (`/`-separated) path for `uri`; `''` is the root. */
|
|
274
|
+
#rel(uri) {
|
|
275
|
+
const abs = this.#abs(uri);
|
|
276
|
+
return abs === this.#root ? '' : this.#path.relative(this.#root, abs).split(this.#path.sep).join('/');
|
|
277
|
+
}
|
|
278
|
+
#toUri(rel) {
|
|
279
|
+
const clean = rel.replace(/^\/+/, '');
|
|
280
|
+
return clean ? `${this.source}://${clean}` : `${this.source}://`;
|
|
281
|
+
}
|
|
282
|
+
/** A machine path under the root → its uri; `undefined` outside it. */
|
|
283
|
+
#uriOfPath(abs) {
|
|
284
|
+
const p = this.#path;
|
|
285
|
+
if (abs !== this.#root && !isPathInside(this.#root, abs, p))
|
|
286
|
+
return undefined;
|
|
287
|
+
return this.#toUri(abs === this.#root ? '' : p.relative(this.#root, abs).split(p.sep).join('/'));
|
|
288
|
+
}
|
|
289
|
+
/** `base` (root-relative) joined with a walk-relative `rel`. */
|
|
290
|
+
static #join(base, rel) {
|
|
291
|
+
return base ? (rel ? `${base}/${rel}` : base) : rel;
|
|
292
|
+
}
|
|
293
|
+
async probeConnection() {
|
|
294
|
+
return this.#transport.probe();
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Why a device folder could not serve a call that threw: the plane is down —
|
|
298
|
+
* answered by the SAME probe the listing's `live`/`live_reason` comes from —
|
|
299
|
+
* or the device refused it by its fixed code. `undefined` for an in-process
|
|
300
|
+
* folder and for an error that is no transport refusal: the caller keeps its
|
|
301
|
+
* own answer. One probe on a failure path only (the device verdict is cached).
|
|
302
|
+
*/
|
|
303
|
+
async unavailable(error) {
|
|
304
|
+
if (this.#transport.kind !== 'device')
|
|
305
|
+
return undefined;
|
|
306
|
+
const refused = transportRefusalReason(error);
|
|
307
|
+
if (refused === undefined)
|
|
308
|
+
return undefined;
|
|
309
|
+
const probe = await this.#transport.probe();
|
|
310
|
+
if (probe.connected)
|
|
311
|
+
return { refused };
|
|
312
|
+
return { live: false, reason: probe.reason ?? 'store_unreachable', ...(probe.notReady ? { notReady: probe.notReady } : {}) };
|
|
313
|
+
}
|
|
314
|
+
/** Wiring diagnostics (operator log only — the root names a machine path). */
|
|
315
|
+
async probeRoot() {
|
|
316
|
+
const probe = await this.#transport.probe();
|
|
317
|
+
return {
|
|
318
|
+
rootPath: this.#root,
|
|
319
|
+
exists: probe.connected || probe.reason !== 'fs_unreachable',
|
|
320
|
+
readable: probe.connected,
|
|
321
|
+
...(probe.connected ? {} : { error: probe.reason ?? 'unreachable' }),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async list(uri) {
|
|
325
|
+
const rel = this.#rel(uri);
|
|
326
|
+
let entries;
|
|
327
|
+
try {
|
|
328
|
+
entries = await this.#transport.list(this.#abs(uri));
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
const code = rootAccessCode(error);
|
|
332
|
+
if (rel === '' && code)
|
|
333
|
+
throw new HostRootAccessError(code, this.#root, uri, error);
|
|
334
|
+
if (isSkippableFsError(error))
|
|
335
|
+
return [];
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
return entries
|
|
339
|
+
.map((entry) => ({
|
|
340
|
+
uri: this.#toUri(_a.#join(rel, entry.name)),
|
|
341
|
+
name: entry.name,
|
|
342
|
+
isDirectory: entry.isDirectory,
|
|
343
|
+
...(entry.size === undefined ? {} : { size: entry.size }),
|
|
344
|
+
...(entry.modifiedAt ? { modifiedAt: entry.modifiedAt } : {}),
|
|
345
|
+
}))
|
|
346
|
+
.sort((a, b) => (a.isDirectory !== b.isDirectory ? (a.isDirectory ? -1 : 1) : a.name.localeCompare(b.name)));
|
|
347
|
+
}
|
|
348
|
+
async stat(uri) {
|
|
349
|
+
return this.#transport.stat(this.#abs(uri));
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* The whole file at `abs`, `size` bytes as `stat` saw it, in transport-sized
|
|
353
|
+
* ranges. A file whose size moves between ranges, or that grew past `size`
|
|
354
|
+
* (one probe byte at the end), is refused — never returned torn.
|
|
355
|
+
*/
|
|
356
|
+
async #readAll(abs, size, uri, tooLarge) {
|
|
357
|
+
if (size <= this.#transport.maxRangeBytes) {
|
|
358
|
+
// One range holds the file: its buffer IS the answer — no second copy.
|
|
359
|
+
// The +1 asks one byte past the size, so growth shows as a longer answer.
|
|
360
|
+
const range = await this.#transport.readRange(abs, 0, size + 1);
|
|
361
|
+
if (range.size !== size || range.bytes.length !== size) {
|
|
362
|
+
throw range.size > size || range.bytes.length > size ? tooLarge() : new Error(`File changed while reading: "${uri}"`);
|
|
363
|
+
}
|
|
364
|
+
return Buffer.from(range.bytes.buffer, range.bytes.byteOffset, range.bytes.byteLength);
|
|
365
|
+
}
|
|
366
|
+
const out = Buffer.allocUnsafe(size);
|
|
367
|
+
let filled = 0;
|
|
368
|
+
while (filled < size) {
|
|
369
|
+
const range = await this.#transport.readRange(abs, filled, Math.min(this.#transport.maxRangeBytes, size - filled));
|
|
370
|
+
if (range.size !== size)
|
|
371
|
+
throw range.size > size ? tooLarge() : new Error(`File changed while reading: "${uri}"`);
|
|
372
|
+
if (range.bytes.length === 0)
|
|
373
|
+
throw new Error(`File changed while reading: "${uri}"`);
|
|
374
|
+
out.set(range.bytes, filled);
|
|
375
|
+
filled += range.bytes.length;
|
|
376
|
+
}
|
|
377
|
+
const tail = await this.#transport.readRange(abs, size, 1);
|
|
378
|
+
if (tail.bytes.length > 0 || tail.size !== size)
|
|
379
|
+
throw tail.size > size ? tooLarge() : new Error(`File changed while reading: "${uri}"`);
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
/** Run `fn` on a real path of this file on THIS machine — the guarded path, or a private temp copy of a device file. */
|
|
383
|
+
async #withLocalFile(abs, size, uri, fn) {
|
|
384
|
+
if (this.#transport.localPath)
|
|
385
|
+
return fn(await this.#transport.localPath(abs));
|
|
386
|
+
const bytes = await this.#readAll(abs, size, uri, () => new Error(`File changed while reading: "${uri}"`));
|
|
387
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'neuralis-hostfile-'));
|
|
388
|
+
try {
|
|
389
|
+
const local = path.join(dir, `file${this.#path.extname(abs)}`);
|
|
390
|
+
await fs.writeFile(local, bytes);
|
|
391
|
+
return await fn(local);
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
await fs.rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async #statFile(abs, uri) {
|
|
398
|
+
const st = await this.#transport.stat(abs);
|
|
399
|
+
if (!st)
|
|
400
|
+
throw Object.assign(new Error(`ENOENT: no such file "${uri}"`), { code: 'ENOENT' });
|
|
401
|
+
if (st.isDirectory)
|
|
402
|
+
throw new Error(`Cannot read directory as file: "${uri}"`);
|
|
403
|
+
return st;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* The byte-exact read: the whole file or a refusal, never a prefix or a
|
|
407
|
+
* marker. Cap = `min(opts.maxBytes, brainMaxReadFileMb)`, checked before any
|
|
408
|
+
* byte moves; a file that grows past it while it is read is refused too.
|
|
409
|
+
*/
|
|
410
|
+
async readRaw(uri, opts) {
|
|
411
|
+
const abs = this.#abs(uri);
|
|
412
|
+
const planeCap = maxReadBytes();
|
|
413
|
+
const cap = Math.max(0, Math.min(Math.floor(opts.maxBytes), planeCap));
|
|
414
|
+
const tooLarge = (size) => new ConnectorRawReadTooLarge(uri, cap, {
|
|
415
|
+
...(size === undefined ? {} : { size }),
|
|
416
|
+
...(planeCap < opts.maxBytes ? { planeLimit: planeCap } : {}),
|
|
417
|
+
});
|
|
418
|
+
const st = await this.#statFile(abs, uri);
|
|
419
|
+
const size = st.size ?? 0;
|
|
420
|
+
if (size > cap)
|
|
421
|
+
throw tooLarge(size);
|
|
422
|
+
const bytes = await this.#readAll(abs, size, uri, () => tooLarge());
|
|
423
|
+
return {
|
|
424
|
+
bytes: new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.length),
|
|
425
|
+
size: bytes.length,
|
|
426
|
+
mimeType: binaryMimeTypeForPath(abs) ?? inferMimeTypeFromFilename(abs),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Bytes `[offset, offset+length)` of one file and the WHOLE file's size — the
|
|
431
|
+
* byte lane's `Range` answer. Moves only that range (in transport-sized
|
|
432
|
+
* pieces); the caller bounds `length`. Not a kernel port member.
|
|
433
|
+
*/
|
|
434
|
+
async readByteRange(uri, offset, length) {
|
|
435
|
+
const abs = this.#abs(uri);
|
|
436
|
+
const parts = [];
|
|
437
|
+
let got = 0;
|
|
438
|
+
let size = -1;
|
|
439
|
+
while (got < length) {
|
|
440
|
+
const range = await this.#transport.readRange(abs, offset + got, Math.min(this.#transport.maxRangeBytes, length - got));
|
|
441
|
+
if (size === -1)
|
|
442
|
+
size = range.size;
|
|
443
|
+
else if (range.size !== size)
|
|
444
|
+
throw new Error(`File changed while reading: "${uri}"`);
|
|
445
|
+
if (range.bytes.length === 0)
|
|
446
|
+
break;
|
|
447
|
+
parts.push(range.bytes);
|
|
448
|
+
got += range.bytes.length;
|
|
449
|
+
}
|
|
450
|
+
const bytes = parts.length === 1 ? parts[0] : new Uint8Array(Buffer.concat(parts.map((p) => Buffer.from(p.buffer, p.byteOffset, p.byteLength))));
|
|
451
|
+
return { bytes, size: Math.max(size, 0), mimeType: binaryMimeTypeForPath(abs) ?? inferMimeTypeFromFilename(abs) };
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* The MODEL read: text decoded, a PDF as its text (or its rendered first
|
|
455
|
+
* page, opt-in), an image/video/audio inline as base64 under its per-MIME
|
|
456
|
+
* cap and as a `[binary file too large…]` marker above it. Same answer on
|
|
457
|
+
* both transports — the PDF helpers run on THIS server over a private copy.
|
|
458
|
+
*/
|
|
459
|
+
async read(uri) {
|
|
460
|
+
const abs = this.#abs(uri);
|
|
461
|
+
const st = await this.#statFile(abs, uri);
|
|
462
|
+
const size = st.size ?? 0;
|
|
463
|
+
const readCap = maxReadBytes();
|
|
464
|
+
if (size > readCap)
|
|
465
|
+
throw new Error(`File too large (${size} bytes, max ${readCap}): "${uri}"`);
|
|
466
|
+
const tooLarge = () => new Error(`File too large (max ${readCap}): "${uri}"`);
|
|
467
|
+
const binaryMimeType = binaryMimeTypeForPath(abs);
|
|
468
|
+
if (binaryMimeType === 'application/pdf') {
|
|
469
|
+
return this.#withLocalFile(abs, size, uri, async (local) => {
|
|
470
|
+
const textExcerpt = (await hasPdftotext()) ? await pdftotextExcerpt(local) : null;
|
|
471
|
+
if (pdfRenderFirstPage() && (await hasPdftoppm())) {
|
|
472
|
+
const rendered = (await pdftoppmPages(local, 1, 1))[0]?.png ?? null;
|
|
473
|
+
if (rendered && rendered.length <= getInlineImageCapBytes()) {
|
|
474
|
+
// The rendered-PDF signal: the journal persists these bytes INLINE —
|
|
475
|
+
// re-reading the .pdf would re-render non-deterministically.
|
|
476
|
+
return { text: textExcerpt ?? '', mimeType: 'image/png', sourceMimeType: 'application/pdf', size, bytes: rendered.toString('base64') };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (textExcerpt !== null)
|
|
480
|
+
return { text: textExcerpt, mimeType: binaryMimeType, size };
|
|
481
|
+
return {
|
|
482
|
+
text: '[application/pdf — pdftotext unavailable; install poppler-utils to enable PDF text extraction]',
|
|
483
|
+
mimeType: binaryMimeType,
|
|
484
|
+
size,
|
|
485
|
+
};
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (binaryMimeType) {
|
|
489
|
+
const cap = inlineCapForMime(binaryMimeType);
|
|
490
|
+
if (cap !== null && size <= cap) {
|
|
491
|
+
const bytes = await this.#readAll(abs, size, uri, tooLarge);
|
|
492
|
+
return { text: '', mimeType: binaryMimeType, size, bytes: bytes.toString('base64') };
|
|
493
|
+
}
|
|
494
|
+
if (cap !== null) {
|
|
495
|
+
return {
|
|
496
|
+
text: `[binary file too large to inline: ${binaryMimeType} ${formatBytes(size)} (cap ${formatBytes(cap)})]`,
|
|
497
|
+
mimeType: binaryMimeType,
|
|
498
|
+
size,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
// Other binaries (zip, exe, font, …): no text, no bytes — the use-case downgrades to a marker.
|
|
502
|
+
return { text: '', mimeType: binaryMimeType, size };
|
|
503
|
+
}
|
|
504
|
+
const buffer = await this.#readAll(abs, size, uri, tooLarge);
|
|
505
|
+
try {
|
|
506
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(buffer);
|
|
507
|
+
if (!looksBinaryContent(text))
|
|
508
|
+
return { text, size };
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
if (!(error instanceof TypeError))
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
return { text: `[binary or invalid UTF-8 @ ${uri}: ${size} bytes]`, mimeType: 'application/octet-stream', size };
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* The sync media lane's PDF read (not a kernel port member): the raw bytes'
|
|
518
|
+
* sha256 and pages `first..last` rendered by the ONE renderer — none when the
|
|
519
|
+
* hash equals `unlessHash`. `null` when `pdftoppm` is not installed.
|
|
520
|
+
*/
|
|
521
|
+
async renderPdfPages(uri, opts) {
|
|
522
|
+
const abs = this.#abs(uri);
|
|
523
|
+
const st = await this.#statFile(abs, uri);
|
|
524
|
+
const size = st.size ?? 0;
|
|
525
|
+
const readCap = maxReadBytes();
|
|
526
|
+
if (size > readCap)
|
|
527
|
+
throw new Error(`File too large (${size} bytes, max ${readCap}): "${uri}"`);
|
|
528
|
+
if (!(await hasPdftoppm()))
|
|
529
|
+
return null;
|
|
530
|
+
return this.#withLocalFile(abs, size, uri, async (local) => {
|
|
531
|
+
const contentHash = createHash('sha256').update(await fs.readFile(local)).digest('hex');
|
|
532
|
+
if (opts.unlessHash !== undefined && opts.unlessHash === contentHash)
|
|
533
|
+
return { contentHash, pages: [] };
|
|
534
|
+
return { contentHash, pages: await pdftoppmPages(local, opts.first, opts.last) };
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
async write(uri, content) {
|
|
538
|
+
const bytes = typeof content === 'string' ? Buffer.from(content, 'utf-8') : content;
|
|
539
|
+
await this.#transport.write(this.#abs(uri), bytes);
|
|
540
|
+
return { uri };
|
|
541
|
+
}
|
|
542
|
+
async delete(uri) {
|
|
543
|
+
await this.#transport.delete(this.#abs(uri));
|
|
544
|
+
}
|
|
545
|
+
async move(fromUri, toUri) {
|
|
546
|
+
await this.#transport.move(this.#abs(fromUri), this.#abs(toUri));
|
|
547
|
+
return { uri: toUri, previousUri: fromUri };
|
|
548
|
+
}
|
|
549
|
+
async mkdir(uri) {
|
|
550
|
+
await this.#transport.mkdir(this.#abs(uri));
|
|
551
|
+
}
|
|
552
|
+
async rmdir(uri) {
|
|
553
|
+
await this.#transport.rmdir(this.#abs(uri));
|
|
554
|
+
}
|
|
555
|
+
async walk(rootUri, opts) {
|
|
556
|
+
return (await this.#walk(rootUri, opts, false)).items;
|
|
557
|
+
}
|
|
558
|
+
/** `complete` proves absence; `walkWithStatus` exists on BOTH transports (the export's verifier reads it). */
|
|
559
|
+
async walkWithStatus(rootUri, opts) {
|
|
560
|
+
return this.#walk(rootUri, opts, true);
|
|
561
|
+
}
|
|
562
|
+
async #walk(rootUri, opts, diagnostic) {
|
|
563
|
+
// A trailing slash on the walk root never seeds a `//` into child uris.
|
|
564
|
+
const base = this.#rel(rootUri);
|
|
565
|
+
const positive = (n) => typeof n === 'number' && Number.isFinite(n) && n > 0 ? Math.min(Math.floor(n), DEFAULT_MAX_SCAN_FILES) : undefined;
|
|
566
|
+
const maxItems = positive(opts?.maxItems) ?? (diagnostic ? DEFAULT_MAX_SCAN_FILES : 500);
|
|
567
|
+
const maxEntries = positive(opts?.maxEntries) ?? positive(opts?.maxItems) ?? 500;
|
|
568
|
+
let result;
|
|
569
|
+
try {
|
|
570
|
+
result = await this.#transport.walk(this.#abs(this.#toUri(base)), {
|
|
571
|
+
maxItems,
|
|
572
|
+
...(diagnostic ? { maxEntries } : {}),
|
|
573
|
+
// No default excludes — a walk is the physical tree.
|
|
574
|
+
...(opts?.excludeGlobs?.length ? { excludeGlobs: opts.excludeGlobs } : {}),
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
if (diagnostic || isSkippableFsError(error))
|
|
579
|
+
return { items: [], complete: false, truncated: false };
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
items: result.entries.map((entry) => ({
|
|
584
|
+
uri: this.#toUri(_a.#join(base, entry.rel)),
|
|
585
|
+
name: entry.name,
|
|
586
|
+
isDirectory: entry.isDirectory,
|
|
587
|
+
...(entry.size === undefined ? {} : { size: entry.size }),
|
|
588
|
+
...(entry.modifiedAt ? { modifiedAt: entry.modifiedAt } : {}),
|
|
589
|
+
})),
|
|
590
|
+
complete: result.complete,
|
|
591
|
+
truncated: result.truncated,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
#countKey(rootUri, opts) {
|
|
595
|
+
const cap = typeof opts?.maxScanFiles === 'number' && opts.maxScanFiles > 0 ? opts.maxScanFiles : DEFAULT_MAX_SCAN_FILES;
|
|
596
|
+
return `${rootUri}::${(opts?.excludeGlobs ?? []).join('|')}::${cap}`;
|
|
597
|
+
}
|
|
598
|
+
#countTtl() {
|
|
599
|
+
return getBrainConfigNumber('brainCountCacheTtlMs', undefined, COUNT_CACHE_TTL_MS);
|
|
600
|
+
}
|
|
601
|
+
/** Bounded: past `MAX_COUNT_CACHE_ENTRIES` the oldest goes. An expired entry stays as the stale answer. */
|
|
602
|
+
#rememberCount(key, entry) {
|
|
603
|
+
this.#countCache.delete(key);
|
|
604
|
+
if (this.#countCache.size >= MAX_COUNT_CACHE_ENTRIES)
|
|
605
|
+
this.#countCache.delete(this.#countCache.keys().next().value);
|
|
606
|
+
this.#countCache.set(key, { ...entry, ts: Date.now() });
|
|
607
|
+
}
|
|
608
|
+
/** The last `count` for the same root and options while younger than the TTL — never I/O. */
|
|
609
|
+
peekCount(rootUri, opts) {
|
|
610
|
+
const cached = this.#countCache.get(this.#countKey(rootUri, opts));
|
|
611
|
+
if (!cached || Date.now() - cached.ts >= this.#countTtl())
|
|
612
|
+
return undefined;
|
|
613
|
+
return cached.value;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Fresh ⇒ the cached total, or the cached FAILURE rethrown at once (a walk
|
|
617
|
+
* that failed or timed out is not tried again within the TTL; a total from
|
|
618
|
+
* before it is still answered). Expired with a total ⇒ that stale total AT
|
|
619
|
+
* ONCE while one walk refreshes it. Otherwise ⇒ the walk's answer.
|
|
620
|
+
* Concurrent callers of one key share ONE walk.
|
|
621
|
+
*/
|
|
622
|
+
async count(rootUri, opts) {
|
|
623
|
+
const key = this.#countKey(rootUri, opts);
|
|
624
|
+
const cached = this.#countCache.get(key);
|
|
625
|
+
if (cached && Date.now() - cached.ts < this.#countTtl()) {
|
|
626
|
+
if (cached.value !== undefined)
|
|
627
|
+
return cached.value;
|
|
628
|
+
throw cached.error;
|
|
629
|
+
}
|
|
630
|
+
const walk = this.#countWalk(key, rootUri, opts);
|
|
631
|
+
if (cached?.value === undefined)
|
|
632
|
+
return walk;
|
|
633
|
+
// The stale total answers this call; the walk's outcome (total or failure) is remembered for the next.
|
|
634
|
+
walk.catch(() => undefined);
|
|
635
|
+
return cached.value;
|
|
636
|
+
}
|
|
637
|
+
#countWalk(key, rootUri, opts) {
|
|
638
|
+
const running = this.#countWalks.get(key);
|
|
639
|
+
if (running)
|
|
640
|
+
return running;
|
|
641
|
+
const cap = typeof opts?.maxScanFiles === 'number' && opts.maxScanFiles > 0 ? opts.maxScanFiles : DEFAULT_MAX_SCAN_FILES;
|
|
642
|
+
const walk = (async () => {
|
|
643
|
+
let total;
|
|
644
|
+
try {
|
|
645
|
+
total = await this.#transport.count(this.#abs(rootUri), {
|
|
646
|
+
maxScanFiles: cap,
|
|
647
|
+
...(opts?.excludeGlobs?.length ? { excludeGlobs: opts.excludeGlobs } : {}),
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
if (isSkippableFsError(error))
|
|
652
|
+
return 0;
|
|
653
|
+
const before = this.#countCache.get(key)?.value;
|
|
654
|
+
this.#rememberCount(key, { error, ...(before !== undefined ? { value: before } : {}) });
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
657
|
+
this.#rememberCount(key, { value: total });
|
|
658
|
+
return total;
|
|
659
|
+
})().finally(() => { this.#countWalks.delete(key); });
|
|
660
|
+
this.#countWalks.set(key, walk);
|
|
661
|
+
return walk;
|
|
662
|
+
}
|
|
663
|
+
async scanContent(rootUri, query, opts) {
|
|
664
|
+
if (!query)
|
|
665
|
+
return [];
|
|
666
|
+
const base = this.#rel(rootUri);
|
|
667
|
+
const maxMatches = typeof opts?.maxMatches === 'number' && opts.maxMatches > 0 ? opts.maxMatches : 500;
|
|
668
|
+
let matches;
|
|
669
|
+
try {
|
|
670
|
+
matches = await this.#transport.scanContent(this.#abs(rootUri), query, {
|
|
671
|
+
maxMatches,
|
|
672
|
+
...(opts?.caseSensitive !== undefined ? { caseSensitive: opts.caseSensitive } : {}),
|
|
673
|
+
...(opts?.includeGlobs?.length ? { includeGlobs: opts.includeGlobs } : {}),
|
|
674
|
+
...(opts?.excludeGlobs?.length ? { excludeGlobs: opts.excludeGlobs } : {}),
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
if (isSkippableFsError(error))
|
|
679
|
+
return [];
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
682
|
+
return matches.map((match) => ({
|
|
683
|
+
uri: this.#toUri(_a.#join(base, match.rel)),
|
|
684
|
+
line: match.line,
|
|
685
|
+
...(match.column !== undefined ? { column: match.column } : {}),
|
|
686
|
+
preview: match.preview,
|
|
687
|
+
}));
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Change events under `rootUri` from a watcher on the folder's own machine
|
|
691
|
+
* (this process's `FsWatchHub`, or the device's broker over its link, re-opened
|
|
692
|
+
* on loss with ONE `overflow {watcher_restarted}`). An `overflow` means event
|
|
693
|
+
* delivery can no longer be trusted — the consumer reconciles the source.
|
|
694
|
+
*/
|
|
695
|
+
subscribeEvents(rootUri, listener, opts) {
|
|
696
|
+
const watch = this.#transport.watch(this.#abs(rootUri), (event) => {
|
|
697
|
+
if (event.kind === 'overflow') {
|
|
698
|
+
listener({
|
|
699
|
+
kind: 'overflow',
|
|
700
|
+
uri: `${this.source}://`,
|
|
701
|
+
...(event.at ? { modifiedAt: event.at } : {}),
|
|
702
|
+
...(event.reason ? { reason: event.reason } : {}),
|
|
703
|
+
...(event.code ? { code: event.code } : {}),
|
|
704
|
+
});
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const uri = this.#uriOfPath(this.#path.normalize(event.path));
|
|
708
|
+
if (!uri || uri === `${this.source}://`)
|
|
709
|
+
return;
|
|
710
|
+
listener({ kind: event.kind, uri, ...(event.at ? { modifiedAt: event.at } : {}), ...(event.entry ? { entry: event.entry } : {}) });
|
|
711
|
+
}, {
|
|
712
|
+
...(opts?.excludeGlobs?.length ? { excludeGlobs: opts.excludeGlobs } : {}),
|
|
713
|
+
...(opts?.maxFolders !== undefined ? { maxFolders: opts.maxFolders } : {}),
|
|
714
|
+
});
|
|
715
|
+
return { unsubscribe: () => watch.unsubscribe(), watchState: () => watch.watchState() };
|
|
716
|
+
}
|
|
717
|
+
async scanDelta(request) {
|
|
718
|
+
request.signal?.throwIfAborted();
|
|
719
|
+
const rootUri = request.rootUri ?? `${this.source}://`;
|
|
720
|
+
const base = this.#rel(rootUri);
|
|
721
|
+
const cap = typeof request.maxScanFiles === 'number' && request.maxScanFiles > 0 ? request.maxScanFiles : DEFAULT_MAX_SCAN_FILES;
|
|
722
|
+
const result = await this.#transport.scanDelta(this.#abs(rootUri), {
|
|
723
|
+
maxScanFiles: cap,
|
|
724
|
+
...(request.since !== undefined ? { since: request.since } : {}),
|
|
725
|
+
...(request.excludeGlobs?.length ? { excludeGlobs: request.excludeGlobs } : {}),
|
|
726
|
+
...(request.includeInventory ? { includeInventory: true } : {}),
|
|
727
|
+
...(request.signal ? { signal: request.signal } : {}),
|
|
728
|
+
});
|
|
729
|
+
request.signal?.throwIfAborted();
|
|
730
|
+
const toItem = (entry) => ({
|
|
731
|
+
uri: this.#toUri(_a.#join(base, entry.rel)),
|
|
732
|
+
name: entry.name,
|
|
733
|
+
...(entry.size === undefined ? {} : { size: entry.size }),
|
|
734
|
+
...(entry.modifiedAt ? { modifiedAt: entry.modifiedAt } : {}),
|
|
735
|
+
});
|
|
736
|
+
return {
|
|
737
|
+
files: result.files.map(toItem),
|
|
738
|
+
checkpoint: result.checkpoint,
|
|
739
|
+
truncated: result.truncated,
|
|
740
|
+
...(result.inventory ? { inventory: result.inventory.map(toItem) } : {}),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* The machine path of `uri` (`os_uri`): the device's own path for a device
|
|
745
|
+
* folder, the native path, or a Docker mount's host half. Inside Docker with
|
|
746
|
+
* no host pair it THROWS `OsUriUnresolvableError` — never a container path.
|
|
747
|
+
*/
|
|
748
|
+
resolveOsUri(uri) {
|
|
749
|
+
const rest = this.#rest(uri);
|
|
750
|
+
const fast = rest && this.#osBase !== undefined ? this.#under(this.#osBase, rest) : undefined;
|
|
751
|
+
if (fast !== undefined)
|
|
752
|
+
return fast;
|
|
753
|
+
const abs = this.#abs(uri);
|
|
754
|
+
if (this.#osRoot === undefined)
|
|
755
|
+
throw new OsUriUnresolvableError({ source: this.source, uri, root: this.#root });
|
|
756
|
+
if (this.#osRoot === this.#root)
|
|
757
|
+
return abs;
|
|
758
|
+
const rel = abs === this.#root ? '' : this.#path.relative(this.#root, abs);
|
|
759
|
+
return rel ? this.#path.join(this.#osRoot, rel) : this.#osRoot;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* The package-byte reader of ONE package folder (`folder` is source-relative,
|
|
763
|
+
* `isPackageFolderPath`). Every path must lie inside `<root>/<folder>` —
|
|
764
|
+
* judged lexically in the folder's own flavour before any call, then by the
|
|
765
|
+
* transport's clamp. Nothing is cached or copied: `readText`/`readBytes` are
|
|
766
|
+
* one bounded read each, `list`/`stat` one call each; `available()` is the
|
|
767
|
+
* transport's (the live link table, no RPC); `watch` rides this folder's own
|
|
768
|
+
* watch plus — on a device — the link's come-and-go.
|
|
769
|
+
*/
|
|
770
|
+
packageReader(folder, onReachabilityChange) {
|
|
771
|
+
if (!isPackageFolderPath(folder)) {
|
|
772
|
+
throw new Error(`[brain-core] source "${this.source}": package folder ${JSON.stringify(folder)} is not a source-relative folder.`);
|
|
773
|
+
}
|
|
774
|
+
const p = this.#path;
|
|
775
|
+
const sourceRoot = this.#root;
|
|
776
|
+
const transport = this.#transport;
|
|
777
|
+
const root = folder ? p.join(sourceRoot, ...folder.split('/')) : sourceRoot;
|
|
778
|
+
const inside = (target) => {
|
|
779
|
+
const normal = p.normalize(target);
|
|
780
|
+
if (normal !== root && !isPathInside(root, normal, p))
|
|
781
|
+
throw new Error('[brain-core] a package path left its package folder.');
|
|
782
|
+
return normal;
|
|
783
|
+
};
|
|
784
|
+
const readWhole = async (target, maxBytes) => {
|
|
785
|
+
const abs = inside(target);
|
|
786
|
+
const cap = Math.max(0, Math.min(Math.floor(maxBytes), PACKAGE_FILE_MAX_BYTES));
|
|
787
|
+
const st = await transport.stat(abs);
|
|
788
|
+
if (!st || st.isDirectory)
|
|
789
|
+
throw new Error('[brain-core] a package file is missing.');
|
|
790
|
+
const size = st.size ?? 0;
|
|
791
|
+
if (size > cap)
|
|
792
|
+
throw new Error(`[brain-core] a package file is ${size} bytes, over the ${cap}-byte read limit.`);
|
|
793
|
+
return this.#readAll(abs, size, this.#uriOfPath(abs) ?? this.source, () => new Error(`[brain-core] a package file grew past the ${cap}-byte read limit.`));
|
|
794
|
+
};
|
|
795
|
+
const reader = {
|
|
796
|
+
// Per SOURCE: every uri names its source, so two sources on one folder are two readers.
|
|
797
|
+
id: `${transport.id}:${encodeURIComponent(this.source)}:${root}`,
|
|
798
|
+
transport: transport.kind,
|
|
799
|
+
readText: async (target) => (await readWhole(target, PACKAGE_FILE_MAX_BYTES)).toString('utf-8'),
|
|
800
|
+
readBytes: async (target, maxBytes) => {
|
|
801
|
+
const bytes = await readWhole(target, maxBytes);
|
|
802
|
+
return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.length);
|
|
803
|
+
},
|
|
804
|
+
list: async (dir) => (await transport.list(inside(dir)))
|
|
805
|
+
.map((e) => ({ name: e.name, isDirectory: e.isDirectory, isFile: !e.isDirectory })),
|
|
806
|
+
stat: async (target) => transport.stat(inside(target)),
|
|
807
|
+
uriOf: (target) => {
|
|
808
|
+
try {
|
|
809
|
+
return this.#uriOfPath(inside(target));
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
return undefined;
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
available: () => transport.available(),
|
|
816
|
+
watch: (onChange) => {
|
|
817
|
+
const watch = this.subscribeEvents(this.#toUri(folder), () => onChange(), {
|
|
818
|
+
excludeGlobs: PACKAGE_WATCH_EXCLUDES,
|
|
819
|
+
maxFolders: PACKAGE_FOLDER_WATCH_MAX_FOLDERS,
|
|
820
|
+
});
|
|
821
|
+
const unlink = onReachabilityChange?.(onChange);
|
|
822
|
+
return () => { unlink?.(); watch.unsubscribe(); };
|
|
823
|
+
},
|
|
824
|
+
};
|
|
825
|
+
return { root, reader };
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
_a = HostConnector;
|
|
829
|
+
/**
|
|
830
|
+
* An in-process `host` connector over `root` on this machine. `osRoot` is the
|
|
831
|
+
* root as the machine's user sees it: natively the root itself, in Docker the
|
|
832
|
+
* stored `hostRoot` or the os-mount pair's host half (absent ⇒ `resolveOsUri`
|
|
833
|
+
* throws, never a container path).
|
|
834
|
+
*/
|
|
835
|
+
export function inProcessHostConnector(input) {
|
|
836
|
+
const root = path.resolve(input.root);
|
|
837
|
+
return new HostConnector({
|
|
838
|
+
source: input.source,
|
|
839
|
+
root,
|
|
840
|
+
transport: new InProcessTransport(root),
|
|
841
|
+
...(input.osRoot !== undefined ? { osRoot: path.resolve(input.osRoot) } : {}),
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* The device half of a `host` source: the stored `root` must be absolute ON
|
|
846
|
+
* THAT DEVICE — its flavour is its own shape (`C:\…` Windows, `/…` POSIX; a
|
|
847
|
+
* drive-relative `C:x` or a UNC share is refused) — and the device's transport
|
|
848
|
+
* comes from agent-core. A root whose shape contradicts the platform the device
|
|
849
|
+
* claimed is refused by agent-core (`openTransport`).
|
|
850
|
+
*/
|
|
851
|
+
export function deviceHostRoot(source, rawRoot) {
|
|
852
|
+
const raw = typeof rawRoot === 'string' ? rawRoot : '';
|
|
853
|
+
const isWindows = /^[A-Za-z]:[\\/]/.test(raw);
|
|
854
|
+
const root = isWindows ? windowsRoot(raw) : /^\/+$/.test(raw) ? '/' : raw.replace(/\/+$/, '');
|
|
855
|
+
if (!(isWindows ? root : root.startsWith('/'))) {
|
|
856
|
+
throw new Error(`[brain-core] device folder "${source}": config.root must be an absolute path on that device, got ${JSON.stringify(rawRoot)}`);
|
|
857
|
+
}
|
|
858
|
+
return root;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* The `host` kind's factory — ONE connector class; the STORED config picks the
|
|
862
|
+
* transport (the kernel's `sourcePlane` reads the same field). A device folder
|
|
863
|
+
* without agent-core loaded cannot be served — the binding fails and the source
|
|
864
|
+
* reads as unavailable.
|
|
865
|
+
*/
|
|
866
|
+
export const createHostConnector = (ctx) => {
|
|
867
|
+
const cfg = ctx.config;
|
|
868
|
+
if (sourcePlane(HOST_SOURCE_KIND, ctx.config) === 'device') {
|
|
869
|
+
const state = ctx.packageState();
|
|
870
|
+
const devices = state.ctx?.packages?.getPackageApi('@neuralis/agent-core')?.devices;
|
|
871
|
+
if (!devices)
|
|
872
|
+
throw new Error(`[brain-core] device folder "${ctx.source}": the device plane (agent-core) is not loaded.`);
|
|
873
|
+
const root = deviceHostRoot(ctx.source, cfg.root);
|
|
874
|
+
// A PROJECT-scope folder carries its project: agent-core re-asks, at every
|
|
875
|
+
// use, whether the device's owner still holds `device.own` there.
|
|
876
|
+
const transport = devices.openTransport({
|
|
877
|
+
source: ctx.source,
|
|
878
|
+
device: String(cfg.device),
|
|
879
|
+
root,
|
|
880
|
+
...(ctx.scope.kind === 'project' ? { project: { projectId: ctx.projectId } } : {}),
|
|
881
|
+
});
|
|
882
|
+
return new HostConnector({ source: ctx.source, root, transport, osRoot: root });
|
|
883
|
+
}
|
|
884
|
+
if (typeof cfg.root !== 'string' || cfg.root.length === 0) {
|
|
885
|
+
throw new Error(`[brain-core] host source "${ctx.source}": config.root is required.`);
|
|
886
|
+
}
|
|
887
|
+
const isDocker = detectDocker();
|
|
888
|
+
const { resolvedRoot } = resolveLocalRoot({
|
|
889
|
+
configured: cfg.root,
|
|
890
|
+
hostRoot: cfg.hostRoot,
|
|
891
|
+
projectRoot: ctx.projectRoot ?? '',
|
|
892
|
+
isDocker,
|
|
893
|
+
source: ctx.source,
|
|
894
|
+
});
|
|
895
|
+
// In a container the machine half is the stored `hostRoot`, else the
|
|
896
|
+
// `NEURALIS_MOUNT_*` os-mount pair's; natively it IS the resolved root, so an
|
|
897
|
+
// `os_uri` never names a path a moved home left behind.
|
|
898
|
+
const osRoot = isDocker
|
|
899
|
+
? (typeof cfg.hostRoot === 'string' ? cfg.hostRoot : undefined) ?? deriveHostRootFromOsMounts(resolvedRoot, discoverOsMounts(process.env))
|
|
900
|
+
: resolvedRoot;
|
|
901
|
+
return inProcessHostConnector({ source: ctx.source, root: resolvedRoot, ...(osRoot !== undefined ? { osRoot } : {}) });
|
|
902
|
+
};
|
|
903
|
+
//# sourceMappingURL=HostConnector.js.map
|