@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
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* How a connector kind's config fields render in the Add-Source form — derived
|
|
3
3
|
* from the DECLARATION, never from the kind name and never from `isDocker`.
|
|
4
|
-
* The ROOT field first; the other fields' closed control set is `fieldControl`
|
|
5
|
-
* below.
|
|
6
4
|
*
|
|
7
|
-
*
|
|
8
|
-
* `x-neuralis.ui
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `/home/you/projects` (warded-wren, closing the UI half of F-B5).
|
|
5
|
+
* The form is one page, in the order a person decides: the kind, then WHERE
|
|
6
|
+
* (a field the declaration marks `x-neuralis.ui.transport` — the `host` kind's
|
|
7
|
+
* `device`), then the ROOT folder (with Browse when the root declares
|
|
8
|
+
* `ui.browse`), then the plain fields, and the fields marked `ui.advanced`
|
|
9
|
+
* folded under "Advanced". `configFields` returns that order.
|
|
13
10
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* (`kind === 'host' || kind.startsWith('host-')`) whose own docstring says a
|
|
26
|
-
* future `ssh`/`smb` connector joins the plane by being added to its body. A
|
|
27
|
-
* negative test on that axis would hand the container chrome to every future
|
|
28
|
-
* non-`local` kind — re-creating, in the UI, the kind-blindness this replaces.
|
|
11
|
+
* The root's meaning follows the transport, not a control name: until the
|
|
12
|
+
* transport field holds a value the root is a path in THIS server's filesystem
|
|
13
|
+
* (`container` — the `/mounts` suggestions and the mount-CLI callout apply in
|
|
14
|
+
* Docker); with a device chosen it is a path ON THAT DEVICE
|
|
15
|
+
* (`deviceLabel`, and `devicePlaceholders[<the device's platform>]` else
|
|
16
|
+
* `devicePlaceholder`), browsed through that device's broker and bounded by
|
|
17
|
+
* the folders its owner allowed. A root control is `container-path`
|
|
18
|
+
* or the pre-existing bare `path`, so an externally authored connector keeps
|
|
19
|
+
* working unchanged. Keyed on the DECLARATION and the form's own values, never
|
|
20
|
+
* on a kind name — a name test would hand one kind's chrome to every future
|
|
21
|
+
* kind it does not list.
|
|
29
22
|
*/
|
|
30
23
|
export type SourceKindFormMeta = {
|
|
31
24
|
kind: string;
|
|
@@ -39,6 +32,8 @@ export type RootFieldMeta = {
|
|
|
39
32
|
container: boolean;
|
|
40
33
|
label: string;
|
|
41
34
|
placeholder: string;
|
|
35
|
+
/** The root declares `ui.browse`: the form offers the folder picker (on this server, or on the chosen device). */
|
|
36
|
+
browse: boolean;
|
|
42
37
|
};
|
|
43
38
|
|
|
44
39
|
type SchemaShape = { required: string[]; properties: Record<string, any> };
|
|
@@ -52,21 +47,94 @@ export function schemaShape(meta: SourceKindFormMeta | null | undefined): Schema
|
|
|
52
47
|
};
|
|
53
48
|
}
|
|
54
49
|
|
|
55
|
-
const PATH_CONTROLS = new Set(['path', 'container-path'
|
|
50
|
+
const PATH_CONTROLS = new Set(['path', 'container-path']);
|
|
56
51
|
|
|
57
|
-
|
|
52
|
+
/**
|
|
53
|
+
* `platform` is the chosen device's claimed platform (its WHERE option's
|
|
54
|
+
* `platform`): the placeholder takes the declared `devicePlaceholders` entry
|
|
55
|
+
* for it, else the `devicePlaceholder` default — so a Windows device hints
|
|
56
|
+
* `C:\…`, never another machine's path style.
|
|
57
|
+
*/
|
|
58
|
+
export function rootFieldMeta(
|
|
59
|
+
meta: SourceKindFormMeta | null | undefined,
|
|
60
|
+
values: Readonly<Record<string, unknown>> = {},
|
|
61
|
+
platform?: string,
|
|
62
|
+
): RootFieldMeta {
|
|
58
63
|
const schema = schemaShape(meta);
|
|
59
64
|
const root = schema.properties.root;
|
|
60
65
|
const ui = root?.['x-neuralis']?.ui;
|
|
61
66
|
const control = typeof ui?.control === 'string' ? (ui.control as string) : undefined;
|
|
67
|
+
const onDevice = transportChosen(meta, values);
|
|
68
|
+
const pick = (deviceKey: string, key: string, fallback: string): string =>
|
|
69
|
+
onDevice && typeof ui?.[deviceKey] === 'string' ? ui[deviceKey] : typeof ui?.[key] === 'string' ? ui[key] : fallback;
|
|
70
|
+
const perPlatform = onDevice && platform ? ui?.devicePlaceholders?.[platform] : undefined;
|
|
62
71
|
return {
|
|
63
72
|
needed: Boolean(root && (schema.required.includes('root') || (control && PATH_CONTROLS.has(control)))),
|
|
64
|
-
container:
|
|
65
|
-
label:
|
|
66
|
-
placeholder: typeof
|
|
73
|
+
container: !onDevice,
|
|
74
|
+
label: pick('deviceLabel', 'label', 'Path'),
|
|
75
|
+
placeholder: typeof perPlatform === 'string' ? perPlatform : pick('devicePlaceholder', 'placeholder', ''),
|
|
76
|
+
browse: ui?.browse === true,
|
|
67
77
|
};
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
/** One non-root config field, with the hints the form reads off its `x-neuralis.ui`. */
|
|
81
|
+
export type ConfigField = { key: string; prop: unknown; label: string; help?: string; placeholder?: string };
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The non-root config fields in the order the form renders them: `transport`
|
|
85
|
+
* (WHERE — before the root, because it decides what the root means), `fields`,
|
|
86
|
+
* and `advanced` (`ui.advanced`, folded). A field the schema's exclusion hides
|
|
87
|
+
* for the current values (`hiddenByExclusion`) is not offered at all.
|
|
88
|
+
*/
|
|
89
|
+
export function configFields(
|
|
90
|
+
meta: SourceKindFormMeta | null | undefined,
|
|
91
|
+
values: Readonly<Record<string, unknown>>,
|
|
92
|
+
): { transport: ConfigField[]; fields: ConfigField[]; advanced: ConfigField[] } {
|
|
93
|
+
const out = { transport: [] as ConfigField[], fields: [] as ConfigField[], advanced: [] as ConfigField[] };
|
|
94
|
+
for (const [key, prop] of Object.entries(schemaShape(meta).properties)) {
|
|
95
|
+
if (key === 'root' || hiddenByExclusion(meta, values, key)) continue;
|
|
96
|
+
const ui = prop?.['x-neuralis']?.ui ?? {};
|
|
97
|
+
const field: ConfigField = {
|
|
98
|
+
key,
|
|
99
|
+
prop,
|
|
100
|
+
label: typeof ui.label === 'string' ? ui.label : key,
|
|
101
|
+
...(typeof ui.help === 'string' ? { help: ui.help } : {}),
|
|
102
|
+
...(typeof ui.placeholder === 'string' ? { placeholder: ui.placeholder } : {}),
|
|
103
|
+
};
|
|
104
|
+
(ui.transport === true ? out.transport : ui.advanced === true ? out.advanced : out.fields).push(field);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The kind's one-line card text (`configSchema["x-neuralis"].ui.summary`); the long `description` is the agent's. */
|
|
110
|
+
export function kindSummary(meta: SourceKindFormMeta | null | undefined): string | null {
|
|
111
|
+
const summary = (meta as any)?.configSchema?.['x-neuralis']?.ui?.summary;
|
|
112
|
+
return typeof summary === 'string' && summary ? summary : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Has the form chosen a TRANSPORT — a value in a field the declaration marks
|
|
117
|
+
* `x-neuralis.ui.transport` (the `host` kind's `device`)? Then the root is a
|
|
118
|
+
* path on that device, not in this server's filesystem.
|
|
119
|
+
*/
|
|
120
|
+
export function transportChosen(meta: SourceKindFormMeta | null | undefined, values: Readonly<Record<string, unknown>>): boolean {
|
|
121
|
+
const { properties } = schemaShape(meta);
|
|
122
|
+
return Object.entries(properties).some(([key, prop]) =>
|
|
123
|
+
prop?.['x-neuralis']?.ui?.transport === true && typeof values[key] === 'string' && values[key] !== '');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Is this field hidden because a field it is mutually exclusive with (the
|
|
128
|
+
* schema's `not: { required: [...] }`) already holds a value? The kind's
|
|
129
|
+
* validator refuses the pair, so the form never offers it.
|
|
130
|
+
*/
|
|
131
|
+
export function hiddenByExclusion(meta: SourceKindFormMeta | null | undefined, values: Readonly<Record<string, unknown>>, key: string): boolean {
|
|
132
|
+
const exclusive = (meta as any)?.configSchema?.not?.required;
|
|
133
|
+
if (!Array.isArray(exclusive) || !exclusive.includes(key)) return false;
|
|
134
|
+
return exclusive.some((other: unknown) => other !== key && typeof other === 'string'
|
|
135
|
+
&& values[other] !== undefined && values[other] !== '');
|
|
136
|
+
}
|
|
137
|
+
|
|
70
138
|
/**
|
|
71
139
|
* How a NON-root config field renders. A CLOSED set: `text` (the default — a
|
|
72
140
|
* plain input, `number` for a numeric type) and `select`, whose options a
|
|
@@ -81,7 +149,7 @@ export function rootFieldMeta(meta: SourceKindFormMeta | null | undefined): Root
|
|
|
81
149
|
*/
|
|
82
150
|
export type FieldControl =
|
|
83
151
|
| { control: 'text'; numeric: boolean; refused?: string }
|
|
84
|
-
| { control: 'select'; numeric: boolean; options: OptionsRoute };
|
|
152
|
+
| { control: 'select'; numeric: boolean; options: OptionsRoute; emptyLabel?: string };
|
|
85
153
|
|
|
86
154
|
/** A validated options route: one package-relative path + its query. */
|
|
87
155
|
export type OptionsRoute = { slug: string; path: string; params: Record<string, string> };
|
|
@@ -90,7 +158,7 @@ const ROUTE_PATH = /^[A-Za-z0-9][A-Za-z0-9._~-]*(?:\/[A-Za-z0-9._~-]+)*$/;
|
|
|
90
158
|
const ROUTE_QUERY_PART = /^[A-Za-z0-9._~-]+$/;
|
|
91
159
|
|
|
92
160
|
/**
|
|
93
|
-
* The `/api/packages/<slug>` segment of a registry id: the
|
|
161
|
+
* The `/api/packages/<slug>` segment of a registry id: the native scope is
|
|
94
162
|
* dropped (`@neuralis/agent-core` → `agent-core`, the same rule the route
|
|
95
163
|
* gateway resolves), any other id travels as ONE encoded segment.
|
|
96
164
|
*/
|
|
@@ -128,24 +196,33 @@ export function parseOptionsRoute(raw: unknown, packageId: string): OptionsRoute
|
|
|
128
196
|
return { slug: packageRouteSlug(packageId), path: path as string, params };
|
|
129
197
|
}
|
|
130
198
|
|
|
199
|
+
/**
|
|
200
|
+
* An OPTIONAL select (`ui.optional`) offers its `emptyLabel` as a real choice —
|
|
201
|
+
* the empty value (for the `host` kind's `device`: "This server"); a required
|
|
202
|
+
* one only lists the route's rows.
|
|
203
|
+
*/
|
|
131
204
|
export function fieldControl(prop: unknown, packageId: string): FieldControl {
|
|
132
|
-
const p = (prop ?? {}) as { type?: unknown; 'x-neuralis'?: { ui?: { control?: unknown; optionsRoute?: unknown } } };
|
|
205
|
+
const p = (prop ?? {}) as { type?: unknown; 'x-neuralis'?: { ui?: { control?: unknown; optionsRoute?: unknown; optional?: unknown; emptyLabel?: unknown } } };
|
|
133
206
|
const numeric = p.type === 'number' || p.type === 'integer';
|
|
134
207
|
const ui = p['x-neuralis']?.ui;
|
|
135
208
|
if (ui?.control !== 'select') return { control: 'text', numeric };
|
|
136
209
|
const route = parseOptionsRoute(ui.optionsRoute, packageId);
|
|
137
210
|
if ('refused' in route) return { control: 'text', numeric, refused: route.refused };
|
|
138
|
-
|
|
211
|
+
const emptyLabel = ui.optional === true ? (typeof ui.emptyLabel === 'string' && ui.emptyLabel ? ui.emptyLabel : 'None') : undefined;
|
|
212
|
+
return { control: 'select', numeric, options: route, ...(emptyLabel ? { emptyLabel } : {}) };
|
|
139
213
|
}
|
|
140
214
|
|
|
141
|
-
/** One row of a `select`: the stored value, its label,
|
|
142
|
-
export type SelectOption = { value: string; label: string; dimmed: boolean };
|
|
215
|
+
/** One row of a `select`: the stored value, its label, whether it reads dimmed, and a device row's claimed `platform`. */
|
|
216
|
+
export type SelectOption = { value: string; label: string; dimmed: boolean; platform?: string };
|
|
217
|
+
|
|
218
|
+
/** The OS word a device row's `platform` reads as; any other token prints as it came. */
|
|
219
|
+
const PLATFORM_WORDS: Readonly<Record<string, string>> = { win32: 'Windows', darwin: 'macOS', linux: 'Linux' };
|
|
143
220
|
|
|
144
221
|
/**
|
|
145
222
|
* The options an `optionsRoute` answered. The answer is a JSON object whose
|
|
146
|
-
* FIRST array-valued property lists the rows; a row is `{id, name?,
|
|
147
|
-
* confinement?}` (the shape of a device list). The label is
|
|
148
|
-
* `name · status · confinement`; a `revoked` row is not offered at all; any
|
|
223
|
+
* FIRST array-valued property lists the rows; a row is `{id, name?, platform?,
|
|
224
|
+
* status?, confinement?}` (the shape of a device list). The label is
|
|
225
|
+
* `name · OS · status · confinement`; a `revoked` row is not offered at all; any
|
|
149
226
|
* status other than `online` reads dimmed but stays selectable — attaching a
|
|
150
227
|
* source to an offline device is allowed, it simply answers once the device is
|
|
151
228
|
* back. Rows without a string `id` are dropped.
|
|
@@ -157,14 +234,16 @@ export function selectOptionsFrom(answer: unknown): SelectOption[] {
|
|
|
157
234
|
const out: SelectOption[] = [];
|
|
158
235
|
for (const row of rows) {
|
|
159
236
|
if (!row || typeof row !== 'object') continue;
|
|
160
|
-
const r = row as { id?: unknown; name?: unknown; status?: unknown; confinement?: unknown };
|
|
237
|
+
const r = row as { id?: unknown; name?: unknown; platform?: unknown; status?: unknown; confinement?: unknown };
|
|
161
238
|
if (typeof r.id !== 'string' || r.id.length === 0) continue;
|
|
162
239
|
const status = typeof r.status === 'string' ? r.status : undefined;
|
|
163
240
|
if (status === 'revoked') continue;
|
|
241
|
+
const platform = typeof r.platform === 'string' && r.platform ? r.platform : undefined;
|
|
164
242
|
const parts = [typeof r.name === 'string' && r.name ? r.name : r.id];
|
|
243
|
+
if (platform) parts.push(PLATFORM_WORDS[platform] ?? platform);
|
|
165
244
|
if (status) parts.push(status.replace(/_/g, ' '));
|
|
166
245
|
if (typeof r.confinement === 'string') parts.push(r.confinement);
|
|
167
|
-
out.push({ value: r.id, label: parts.join(' · '), dimmed: status !== undefined && status !== 'online' });
|
|
246
|
+
out.push({ value: r.id, label: parts.join(' · '), dimmed: status !== undefined && status !== 'online', ...(platform ? { platform } : {}) });
|
|
168
247
|
}
|
|
169
248
|
return out;
|
|
170
249
|
}
|
|
@@ -326,27 +326,121 @@ export function getSourceVisualTone(info: SourceInfo): {
|
|
|
326
326
|
// gone. Returned as a plain string for the parent button's `title` tooltip.
|
|
327
327
|
/** Display strings for the `SourceLiveReason` enum (the wire value is an enum to
|
|
328
328
|
* keep it parseable / leak-free — the human text is mapped here, in the UI). */
|
|
329
|
+
/** How a device's owner updates the CLI that serves it: the desktop app carries it, an npm install updates by npm (`neuralis update` is the SERVER's verb, never a device's). */
|
|
330
|
+
const DEVICE_UPDATE_STEPS = 'update the Neuralis desktop app on that computer, or run npm i -g @neuralis/cli@latest there, then neuralis device install';
|
|
331
|
+
|
|
329
332
|
const LIVE_REASON_LABEL: Record<string, string> = {
|
|
330
333
|
runtime_offline: 'Machine offline — container not running',
|
|
331
334
|
fs_unreachable: 'Filesystem unreachable — mount missing',
|
|
332
335
|
fs_permission: 'Filesystem unreadable — permission denied',
|
|
333
336
|
store_unreachable: 'Store offline — backing service unreachable',
|
|
337
|
+
broker_outdated: `Device needs an update — ${DEVICE_UPDATE_STEPS}`,
|
|
338
|
+
devices_disabled: 'Devices are turned off on this server — an admin turns them on in Admin → Config',
|
|
339
|
+
device_not_ready: 'Device not ready — neuralis device status on that computer says why',
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/** A `host` folder is offline only through its device's link (an in-process folder answers `fs_*`). */
|
|
343
|
+
const DEVICE_OFFLINE_LABEL = 'Device offline — it is not connected to this server';
|
|
344
|
+
|
|
345
|
+
/** What a device's owner fixes there, per broker readiness code (`DeviceNotReadyCode`). */
|
|
346
|
+
const DEVICE_NOT_READY_LABEL: Record<string, string> = {
|
|
347
|
+
ceiling_missing_or_empty: 'no folder is allowed on it yet (neuralis device grant on that computer)',
|
|
348
|
+
sandbox_helper_missing: 'its sandbox helper is not usable (neuralis device install on that computer)',
|
|
349
|
+
socket_isolation_failed: 'its sandbox cannot keep commands away from the computer\'s own control sockets (neuralis device status on that computer says how to fix it)',
|
|
350
|
+
secret_file_missing_or_too_short: 'its pairing secret is missing (pair it again)',
|
|
351
|
+
scratch_root_missing: 'its scratch folder is missing (neuralis device install on that computer)',
|
|
334
352
|
};
|
|
335
353
|
|
|
336
354
|
/** Map a `SourceLiveReason` (or undefined) to its display string. Shared by the
|
|
337
|
-
* file tree tooltip
|
|
338
|
-
|
|
355
|
+
* file tree tooltip, the Sources panel and the read/preview failures so all
|
|
356
|
+
* render the same text: `kind` words an offline `host` folder as its device,
|
|
357
|
+
* `notReady` names a not-ready device's own codes. */
|
|
358
|
+
export function sourceLiveReasonLabel(reason?: string | null, opts: { kind?: string | null; notReady?: readonly unknown[] | null } = {}): string {
|
|
359
|
+
if (reason === 'runtime_offline' && opts.kind === 'host') return DEVICE_OFFLINE_LABEL;
|
|
360
|
+
if (reason === 'device_not_ready') {
|
|
361
|
+
const known = (opts.notReady ?? []).flatMap((code) => (typeof code === 'string' && DEVICE_NOT_READY_LABEL[code] ? [DEVICE_NOT_READY_LABEL[code]] : []));
|
|
362
|
+
if (known.length > 0) return `Device not ready — ${known.join('; ')}`;
|
|
363
|
+
}
|
|
339
364
|
return (reason ? LIVE_REASON_LABEL[reason] : undefined)
|
|
340
365
|
?? 'Disconnected — connector cannot reach its backing filesystem';
|
|
341
366
|
}
|
|
342
367
|
|
|
368
|
+
/**
|
|
369
|
+
* The source row's live-watch badge: `null` while the watch covers the whole
|
|
370
|
+
* source (or none is meant for it), else the label and the tooltip that says
|
|
371
|
+
* what is and is not live right now.
|
|
372
|
+
*/
|
|
373
|
+
export function sourceWatchBadge(
|
|
374
|
+
watch: string | null | undefined,
|
|
375
|
+
liveReason?: string | null,
|
|
376
|
+
): { label: string; title: string; tone: 'pending' | 'warn' } | null {
|
|
377
|
+
if (watch === 'starting') {
|
|
378
|
+
return {
|
|
379
|
+
label: 'Live: starting',
|
|
380
|
+
title: 'Live changes are starting: the watcher is still walking this source\'s folders. Until it is ready, a change can arrive only with the next reconcile or Sync now.',
|
|
381
|
+
tone: 'pending',
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (watch === 'partial') {
|
|
385
|
+
return {
|
|
386
|
+
label: 'Live: partial',
|
|
387
|
+
title: 'Live changes cover part of this source: it has more folders than the watcher budget (brainWatchMaxFolders) or this computer\'s watch limit allows. Changes in the folders past it, new folders there included, are picked up by the next full reconcile (every 24 h by default) or by Sync now.',
|
|
388
|
+
tone: 'warn',
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (watch === 'off') {
|
|
392
|
+
return {
|
|
393
|
+
label: 'Live: off',
|
|
394
|
+
title: liveReason === 'broker_outdated'
|
|
395
|
+
? `No live changes for this source: its device runs a CLI too old to watch — ${DEVICE_UPDATE_STEPS}. Changes arrive with the regular sync.`
|
|
396
|
+
: `No live changes for this source: its watch is not running (a device may be offline, or run a CLI too old to watch — ${DEVICE_UPDATE_STEPS}). Changes arrive with the regular sync.`,
|
|
397
|
+
tone: 'warn',
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** A device refusal code its owner can act on, in words — the file read, the preview and the source row say it alike. */
|
|
404
|
+
const DEVICE_REFUSAL_LABEL: Record<string, string> = {
|
|
405
|
+
host_fs_denied: 'Not allowed on that device yet — ask there (tray → Allow a folder…, or neuralis device grant); it serves once allowed',
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/** A device's refusal of one file, by its fixed code; `undefined` for anything that is not a code. */
|
|
409
|
+
export function deviceRefusalLabel(reason: unknown): string | undefined {
|
|
410
|
+
if (typeof reason !== 'string' || !/^[a-z][a-z0-9_]{0,63}$/.test(reason)) return undefined;
|
|
411
|
+
return DEVICE_REFUSAL_LABEL[reason] ?? `The device refused this file (${reason})`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** The fixed codes a refused sync persists as `lastSyncResult.error` (a device's refusal codes too, by the map above). */
|
|
415
|
+
const SYNC_REFUSAL_LABEL: Record<string, string> = {
|
|
416
|
+
root_unreachable: 'Sync stopped — the source folder cannot be reached (unmounted, renamed or offline). Nothing was removed from the index.',
|
|
417
|
+
root_empty: 'Sync stopped — the source folder is empty, but the index still holds its files. Nothing was removed. Choose Sync anyway if the folder really is empty.',
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/** A sync error for display: the label of a fixed refusal code, any other text unchanged. */
|
|
421
|
+
export function syncErrorLabel(error: string): string {
|
|
422
|
+
return SYNC_REFUSAL_LABEL[error] ?? DEVICE_REFUSAL_LABEL[error] ?? error;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Why a source that lists nothing is UNAVAILABLE rather than empty: its live
|
|
427
|
+
* state, else the code its walk, count or last sync stopped on (`last_error` —
|
|
428
|
+
* a failed walk or count never sets `sync_status`); `undefined` = truly empty.
|
|
429
|
+
*/
|
|
430
|
+
export function sourceUnavailableLabel(info: SourceInfo): string | undefined {
|
|
431
|
+
if (info.live === false) return sourceLiveReasonLabel(info.live_reason, { kind: info.source_kind, notReady: info.live_not_ready });
|
|
432
|
+
return info.last_error ? syncErrorLabel(info.last_error) : undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
343
435
|
export function resolveSourceToneTitle(info: SourceInfo): string | null {
|
|
344
436
|
if (info.enabled === false) return 'Source disabled';
|
|
345
437
|
if (info.enabled === 'auto' && info.enabled_effective === false) return 'Source auto — currently stopped';
|
|
346
|
-
if (info.live === false) return sourceLiveReasonLabel(info.live_reason);
|
|
347
|
-
if (info.sync_status === 'error') return info.last_error
|
|
438
|
+
if (info.live === false) return sourceLiveReasonLabel(info.live_reason, { kind: info.source_kind, notReady: info.live_not_ready });
|
|
439
|
+
if (info.sync_status === 'error') return info.last_error ? syncErrorLabel(info.last_error) : 'Sync error';
|
|
348
440
|
if (info.sync_status === 'paused') return 'Sync paused';
|
|
349
441
|
if (info.sync_status === 'active') return 'Sync active';
|
|
442
|
+
// A failed walk or count leaves `sync_status` alone: neutral tone, the code as the title.
|
|
443
|
+
if (info.last_error) return syncErrorLabel(info.last_error);
|
|
350
444
|
return null;
|
|
351
445
|
}
|
|
352
446
|
|
|
@@ -65,6 +65,7 @@ Two guards make this safe against the races a plain re-list would lose:
|
|
|
65
65
|
- **`pendingSourceSuggestion` is a HANDOFF, not state.** The tree's Discover dock cannot call into `SourcesPanel` — the panel is a conditional mount and does not exist at click time — so the pick travels through the store and the panel consumes it on mount, clearing it in the same tick. It is deliberately OUT of the persist allowlist (a reload that re-opened a prefilled Add-Source form by itself is the ghost class this store already fought) and `loadList`'s context reset clears it beside `workspacePanels`, so a pick can never strand across an agent/project switch and prefill a root the new context does not have.
|
|
66
66
|
- `loadedFolders[uri]` means "this folder HAS been enumerated". Only a successful listing sets it. Marking a source loaded that was never listed (the old `loadList` did this for every source, connector-failed ones included) lets a later absence pass prune everything under it. `false` is a third state, STALE: enumerated once, then taken off screen by a collapse. Every truthiness reader treats it as not loaded; `ensureFolderLoaded` on a stale folder runs ONE narrow authoritative pass over exactly that folder (`runSourceReconcile({ wide: false })`, absence applies) instead of an upsert-only listing, and that pass marks it `true` again; a stale folder put back on screen another way (`autoExpandToFile`, a nav link) is re-listed by the next wide pass. A narrow re-open pass whose listing FAILS leaves the folder stale, with only the source header's "may be out of date" hint; the next collapse and re-open retries it.
|
|
67
67
|
- **Collapse and the bound.** `toggleFolder` collapsing a folder drops every expanded descendant from `expandedFolders` and marks every enumerated folder at or under it stale — so no reconcile keeps listing what nobody can see. The expanded set is capped at `EXPANDED_FOLDERS_CAP` (`listSlice.ts`); `expandPatch` moves each newly expanded folder to most-recent and `boundExpandedFolders` drops the oldest LEAF (never an ancestor of a kept folder, never a source root), marking it stale like a collapse. `autoExpandToFile` is the writer that needs it: it adds a folder chain for every pending write an agent makes. The persisted projection and a rehydrated legacy payload are bounded the same way.
|
|
68
|
+
- **Per-source refresh:** It works on a collapsed source too — an explicit click is a user intent, unlike the automatic event-driven loads, which never list a collapsed source — and it opens the branch so the result is visible.
|
|
68
69
|
|
|
69
70
|
## Tests
|
|
70
71
|
|
|
@@ -4,7 +4,7 @@ import { persist, createJSONStorage } from 'zustand/middleware';
|
|
|
4
4
|
import { createStore, type StoreApi as ZustandStoreApi } from 'zustand/vanilla';
|
|
5
5
|
import type { ClientStateResetScope } from '@neuralis/package-system/contracts';
|
|
6
6
|
import { createPackageApi, type PackageApi } from '@neuralis/package-system/client';
|
|
7
|
-
import type { EditorTabMeta, FileNode, FileSystemState, FSWriteResponse, PersistedFilesContext, PersistedFilesWindow, SidebarView, WorkspacePanelId } from '../domain/types';
|
|
7
|
+
import type { EditorTabMeta, FileNode, FileSystemState, FSWriteResponse, PersistedFilesContext, PersistedFilesWindow, SidebarView, SourceInfo, WorkspacePanelId } from '../domain/types';
|
|
8
8
|
export type { SidebarView, WorkspacePanelId } from '../domain/types';
|
|
9
9
|
import { countPendingChanges, readFiles, uploadFile, writeFile, writeRefusalMessage } from '../api';
|
|
10
10
|
import {
|
|
@@ -21,8 +21,9 @@ import {
|
|
|
21
21
|
sourceRootUri,
|
|
22
22
|
uriAncestors,
|
|
23
23
|
parseFilePrev,
|
|
24
|
+
readFailureMessage,
|
|
24
25
|
} from './helpers';
|
|
25
|
-
import { resolveList, type DiscoverableSourceSuggestion } from '../api';
|
|
26
|
+
import { listDiscoverableSources, resolveList, type DiscoverableSourceSuggestion } from '../api';
|
|
26
27
|
import { searchSliceInitial, createSearchSlice, type SearchSliceActions } from './searchSlice';
|
|
27
28
|
import { boundExpandedFolders, contextResetPatch, createListSlice, persistedContext, sliceFromPersisted, type ListSliceActions } from './listSlice';
|
|
28
29
|
import { gitSliceInitial, createGitSlice, type GitSliceState, type GitSliceActions } from './gitSlice';
|
|
@@ -185,6 +186,38 @@ function repairParentUri(next: Record<string, any>, rawUri: string, prev: any):
|
|
|
185
186
|
return fixed;
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
/**
|
|
190
|
+
* A folder listing carries its source's CURRENT state. The tree's empty row and
|
|
191
|
+
* header read `state.sources`, which otherwise only the sources-only listing
|
|
192
|
+
* writes — so a Refresh that found the device gone would show "No files" until
|
|
193
|
+
* the next background pass. Only the live/sync fields move; counts and every
|
|
194
|
+
* other field stay with the listing that asked for them.
|
|
195
|
+
*/
|
|
196
|
+
export function mergeListedSourceState(current: SourceInfo[], res: { sources?: unknown }): SourceInfo[] {
|
|
197
|
+
const listed = Array.isArray(res.sources) ? (res.sources as SourceInfo[]) : [];
|
|
198
|
+
if (listed.length === 0 || current.length === 0) return current;
|
|
199
|
+
let out = current;
|
|
200
|
+
for (const fresh of listed) {
|
|
201
|
+
const i = current.findIndex((s) => s.uri_root === fresh.uri_root);
|
|
202
|
+
if (i < 0) continue;
|
|
203
|
+
const old = current[i];
|
|
204
|
+
const patch: SourceInfo = {
|
|
205
|
+
...old,
|
|
206
|
+
live: fresh.live,
|
|
207
|
+
live_reason: fresh.live_reason,
|
|
208
|
+
live_not_ready: fresh.live_not_ready,
|
|
209
|
+
sync_status: fresh.sync_status,
|
|
210
|
+
last_error: fresh.last_error,
|
|
211
|
+
};
|
|
212
|
+
if (patch.live === old.live && patch.live_reason === old.live_reason
|
|
213
|
+
&& patch.live_not_ready === old.live_not_ready && patch.sync_status === old.sync_status
|
|
214
|
+
&& patch.last_error === old.last_error) continue;
|
|
215
|
+
if (out === current) out = current.slice();
|
|
216
|
+
out[i] = patch;
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
188
221
|
function applyListedNodes(
|
|
189
222
|
next: Record<string, any>,
|
|
190
223
|
res: { folders?: unknown[]; files?: unknown[] },
|
|
@@ -476,6 +509,10 @@ async function runReconcilePass(
|
|
|
476
509
|
const pruned: string[] = [];
|
|
477
510
|
let anyAuthoritative = false;
|
|
478
511
|
let incomplete = false;
|
|
512
|
+
const listedSources = (st: any): SourceInfo[] => listed.reduce(
|
|
513
|
+
(acc: SourceInfo[], entry) => (entry.ok && entry.res ? mergeListedSourceState(acc, entry.res) : acc),
|
|
514
|
+
st.sources ?? [],
|
|
515
|
+
);
|
|
479
516
|
|
|
480
517
|
for (const entry of listed) {
|
|
481
518
|
if (!entry.ok || !entry.res) { incomplete = true; continue; }
|
|
@@ -504,7 +541,7 @@ async function runReconcilePass(
|
|
|
504
541
|
if (!anyAuthoritative) {
|
|
505
542
|
// Every listing failed or was truncated → upsert-only, no absence pass.
|
|
506
543
|
return withPendingDelta(
|
|
507
|
-
{ files: evictColdSubtrees(next, state.expandedFolders, state.openFiles), dirtySources, loadedFolders },
|
|
544
|
+
{ files: evictColdSubtrees(next, state.expandedFolders, state.openFiles), dirtySources, loadedFolders, sources: listedSources(state) },
|
|
508
545
|
state.pendingReviewPaths,
|
|
509
546
|
pendingDelta,
|
|
510
547
|
);
|
|
@@ -605,6 +642,7 @@ async function runReconcilePass(
|
|
|
605
642
|
out.fileContentByPath = cbp; out.fileBaseContentByPath = bbp;
|
|
606
643
|
out.fileErrorByPath = ebp; out.filePrevByPath = pbp;
|
|
607
644
|
}
|
|
645
|
+
out.sources = listedSources(state);
|
|
608
646
|
return withPendingDelta(out, state.pendingReviewPaths, pendingDelta);
|
|
609
647
|
});
|
|
610
648
|
}
|
|
@@ -810,7 +848,7 @@ interface FilesystemStore extends FileSystemState, SearchSliceActions, ListSlice
|
|
|
810
848
|
force?: boolean;
|
|
811
849
|
}) => Promise<void>;
|
|
812
850
|
/**
|
|
813
|
-
*
|
|
851
|
+
* Reconcile the open (expanded) folders of ONE source against
|
|
814
852
|
* the connector's CURRENT truth, in response to an EXTERNAL file change SSE event.
|
|
815
853
|
* Re-lists each expanded+loaded folder under the source and, per direct child
|
|
816
854
|
* absent from the fresh (non-truncated) listing: prunes an `unindexed` external-
|
|
@@ -932,6 +970,22 @@ interface FilesystemStore extends FileSystemState, SearchSliceActions, ListSlice
|
|
|
932
970
|
*/
|
|
933
971
|
pendingSourceSuggestion: DiscoverableSourceSuggestion | null;
|
|
934
972
|
setPendingSourceSuggestion: (suggestion: DiscoverableSourceSuggestion | null) => void;
|
|
973
|
+
/**
|
|
974
|
+
* The attachable-but-unattached source suggestions (`GET sources/discoverable`)
|
|
975
|
+
* for ONE (agent, project) pair — the ONE owner both consumers read: the
|
|
976
|
+
* tree's Discover dock (`useDiscoverableSources`) and the Sources panel (its
|
|
977
|
+
* list + the folder picker's Suggested row). `key` names the pair the answer
|
|
978
|
+
* belongs to; a reader on another pair sees nothing. Not persisted.
|
|
979
|
+
*/
|
|
980
|
+
discoverable: { key: string; suggestions: readonly DiscoverableSourceSuggestion[] } | null;
|
|
981
|
+
/**
|
|
982
|
+
* Fetch the suggestions for a pair: an answer already held for it (or one in
|
|
983
|
+
* flight) is reused, so opening Files costs ONE request whoever asks first;
|
|
984
|
+
* `fresh` re-reads (the panel's Refresh, after a mutation). Fail-soft: a
|
|
985
|
+
* failure holds an empty list for the pair (no retry loop) until the next
|
|
986
|
+
* fresh read or a context change.
|
|
987
|
+
*/
|
|
988
|
+
loadDiscoverable: (ctx: { agentId: string; projectId?: string }, opts?: { fresh?: boolean }) => Promise<readonly DiscoverableSourceSuggestion[]>;
|
|
935
989
|
/**
|
|
936
990
|
* Is the Sources panel's Vector Health section open? In the STORE so a
|
|
937
991
|
* notification's `open-vector-health` nav can open it while the panel is not
|
|
@@ -1147,6 +1201,11 @@ export type FilesystemStoreInit = {
|
|
|
1147
1201
|
|
|
1148
1202
|
export type FilesystemStoreApi = ZustandStoreApi<FilesystemStore>;
|
|
1149
1203
|
|
|
1204
|
+
/** The (agent, project) pair a discoverable answer belongs to. */
|
|
1205
|
+
export function discoverableKey(agentId: string, projectId: string | undefined): string {
|
|
1206
|
+
return `${agentId}\n${projectId ?? ''}`;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1150
1209
|
/**
|
|
1151
1210
|
* ONE Files store per window: a live view that persists nothing itself — the
|
|
1152
1211
|
* ONE writer of `neuralis.filesystem.tabs` is `filesPersistence` below, which a
|
|
@@ -1158,6 +1217,8 @@ export type FilesystemStoreApi = ZustandStoreApi<FilesystemStore>;
|
|
|
1158
1217
|
export function createFilesystemStore(init: FilesystemStoreInit): FilesystemStoreApi {
|
|
1159
1218
|
const fences = createStoreFences();
|
|
1160
1219
|
const client = init.client;
|
|
1220
|
+
// The one discoverable read in flight for this window (`loadDiscoverable`).
|
|
1221
|
+
let discoverableInFlight: { key: string; promise: Promise<readonly DiscoverableSourceSuggestion[]> } | null = null;
|
|
1161
1222
|
return createStore<FilesystemStore>()((set, get) => ({
|
|
1162
1223
|
windowId: init.windowId ?? null,
|
|
1163
1224
|
client,
|
|
@@ -1191,6 +1252,7 @@ export function createFilesystemStore(init: FilesystemStoreInit): FilesystemStor
|
|
|
1191
1252
|
workspacePanels: { upload: false, sources: false, packages: false, pending: false },
|
|
1192
1253
|
activeWorkspaceTab: 'file',
|
|
1193
1254
|
pendingSourceSuggestion: null,
|
|
1255
|
+
discoverable: null,
|
|
1194
1256
|
vectorHealthOpen: false,
|
|
1195
1257
|
diffViewMode: init.diffViewMode ?? 'auto',
|
|
1196
1258
|
pendingPanelFilter: {},
|
|
@@ -1263,6 +1325,24 @@ export function createFilesystemStore(init: FilesystemStoreInit): FilesystemStor
|
|
|
1263
1325
|
return { workspacePanels: nextPanels, activeWorkspaceTab };
|
|
1264
1326
|
}),
|
|
1265
1327
|
setPendingSourceSuggestion: (suggestion) => set(() => ({ pendingSourceSuggestion: suggestion })),
|
|
1328
|
+
loadDiscoverable: (ctx, opts) => {
|
|
1329
|
+
const key = discoverableKey(ctx.agentId, ctx.projectId);
|
|
1330
|
+
const held = get().discoverable;
|
|
1331
|
+
if (!opts?.fresh && held?.key === key) return Promise.resolve(held.suggestions);
|
|
1332
|
+
if (!opts?.fresh && discoverableInFlight?.key === key) return discoverableInFlight.promise;
|
|
1333
|
+
const promise = listDiscoverableSources({ agentId: ctx.agentId, ...(ctx.projectId ? { projectId: ctx.projectId } : {}) }, client)
|
|
1334
|
+
.then((res) => res.suggestions ?? [], () => [] as DiscoverableSourceSuggestion[])
|
|
1335
|
+
.then((suggestions) => {
|
|
1336
|
+
// Only the newest read for a pair writes it; a superseded one answers its caller alone.
|
|
1337
|
+
if (discoverableInFlight?.promise === promise) {
|
|
1338
|
+
discoverableInFlight = null;
|
|
1339
|
+
set(() => ({ discoverable: { key, suggestions } }));
|
|
1340
|
+
}
|
|
1341
|
+
return suggestions as readonly DiscoverableSourceSuggestion[];
|
|
1342
|
+
});
|
|
1343
|
+
discoverableInFlight = { key, promise };
|
|
1344
|
+
return promise;
|
|
1345
|
+
},
|
|
1266
1346
|
setVectorHealthOpen: (open) => set(() => ({ vectorHealthOpen: open })),
|
|
1267
1347
|
setActiveWorkspaceTab: (tab) => set(() => ({ activeWorkspaceTab: tab })),
|
|
1268
1348
|
setDiffViewMode: (mode) => set(() => ({ diffViewMode: mode })),
|
|
@@ -1388,6 +1468,7 @@ export function createFilesystemStore(init: FilesystemStoreInit): FilesystemStor
|
|
|
1388
1468
|
// to list must not have its (never-listed) children pruned.
|
|
1389
1469
|
loadedFolders: { ...(state.loadedFolders || {}), [uri]: true },
|
|
1390
1470
|
hydrationState: { ...(state.hydrationState || {}), [uri]: 'ready' as const },
|
|
1471
|
+
sources: mergeListedSourceState(state.sources ?? [], res),
|
|
1391
1472
|
},
|
|
1392
1473
|
state.pendingReviewPaths,
|
|
1393
1474
|
pendingDelta,
|
|
@@ -2169,7 +2250,7 @@ export function createFilesystemStore(init: FilesystemStoreInit): FilesystemStor
|
|
|
2169
2250
|
if (files.length > 0) {
|
|
2170
2251
|
const f = files[0];
|
|
2171
2252
|
const content = extractFileContent(f);
|
|
2172
|
-
const error =
|
|
2253
|
+
const error = readFailureMessage(f);
|
|
2173
2254
|
// The file is GONE, not merely unreadable. This matters because the open-tab
|
|
2174
2255
|
// shape is PERSISTED: a tab pointing at a deleted file re-materialises its
|
|
2175
2256
|
// node (and its whole folder chain) on every page load, producing a tree row
|
|
@@ -198,6 +198,11 @@ export function createGitSlice(
|
|
|
198
198
|
...(repoRel ? { repo: repoRel } : {}),
|
|
199
199
|
}, get().client);
|
|
200
200
|
if (mySeq !== gitStatusSeq) return;
|
|
201
|
+
if (res?.ok === false) {
|
|
202
|
+
// A checkout git could not read is never shown as "No changes".
|
|
203
|
+
set(() => ({ gitStatus: null, gitTotals: null, gitLoading: false, gitError: res.error }));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
201
206
|
// `totals` rides the SAME response as the rows it sums (the server
|
|
202
207
|
// computes it after its read filter and cap), so the header can never
|
|
203
208
|
// show a number the list below contradicts.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FileNode, FilePrev } from '../domain/types';
|
|
2
|
+
import { deviceRefusalLabel, sourceLiveReasonLabel } from '../lib/treeVisualState';
|
|
2
3
|
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
5
|
// URI helpers (lightweight, client-side — no dependency on brain-core/src/)
|
|
@@ -213,3 +214,19 @@ export function contextKey(
|
|
|
213
214
|
): string {
|
|
214
215
|
return `${userId ?? ''}::${projectId ?? ''}::${agentId}`;
|
|
215
216
|
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* One file's read failure as the Files UI says it: a device folder that could
|
|
220
|
+
* not serve names its state in the tree's own words (`source_unavailable` →
|
|
221
|
+
* the live-reason label, `device_refused` → the refusal code); any other
|
|
222
|
+
* failure keeps the server's text. `undefined` when the file read.
|
|
223
|
+
*/
|
|
224
|
+
export function readFailureMessage(file: { error?: unknown; code?: unknown; reason?: unknown; not_ready?: unknown }): string | undefined {
|
|
225
|
+
if (typeof file.error !== 'string') return undefined;
|
|
226
|
+
// `source_unavailable` is a device folder's answer only (the `host` connector's device transport).
|
|
227
|
+
if (file.code === 'source_unavailable' && typeof file.reason === 'string') {
|
|
228
|
+
return sourceLiveReasonLabel(file.reason, { kind: 'host', notReady: Array.isArray(file.not_ready) ? file.not_ready : null });
|
|
229
|
+
}
|
|
230
|
+
if (file.code === 'device_refused') return deviceRefusalLabel(file.reason) ?? file.error;
|
|
231
|
+
return file.error;
|
|
232
|
+
}
|
|
@@ -436,9 +436,17 @@ export function createListSlice(
|
|
|
436
436
|
}
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
+
// An expanded folder of a source this listing no longer names (deleted, or no
|
|
440
|
+
// longer visible) would be listed on every load and refused: one pass drops it,
|
|
441
|
+
// and the window's persisted projection follows.
|
|
442
|
+
const listedSources = new Set(sources.map((s) => sourceFromUri(s.uri_root)));
|
|
443
|
+
const expanded = state.expandedFolders as Set<string>;
|
|
444
|
+
const keptExpanded = sources.length > 0 ? [...expanded].filter((uri) => listedSources.has(sourceFromUri(uri))) : [];
|
|
445
|
+
|
|
439
446
|
return {
|
|
440
447
|
files: next,
|
|
441
448
|
sources,
|
|
449
|
+
...(sources.length > 0 && keptExpanded.length !== expanded.size ? { expandedFolders: new Set(keptExpanded) } : {}),
|
|
442
450
|
sourcesLoading: false,
|
|
443
451
|
// A listing that answered clears the previous failure's banner.
|
|
444
452
|
snapshotError: undefined,
|