@notis_ai/cli 0.2.17 → 0.2.19
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 +542 -146
- package/bin/check-runtime.js +15 -0
- package/bin/notis.js +2 -0
- package/config/notis_app_design_rules.json +320 -0
- package/config/portal-route-roots.json +45 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +42323 -18258
- package/dist/base-skills/notis-cli/.notis-base-skill.json +1 -0
- package/dist/base-skills/notis-cli/SKILL.md +51 -280
- package/dist/base-skills/notis-cli/references/app-delivery.md +17 -0
- package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
- package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
- package/dist/base-skills/notis-cli/references/troubleshooting.md +67 -0
- package/dist/base-skills/notis-query/.notis-base-skill.json +1 -0
- package/dist/base-skills/notis-query/SKILL.md +35 -699
- package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
- package/dist/base-skills/notis-query/references/documents.md +74 -0
- package/dist/base-skills/notis-query/references/query.md +76 -0
- package/dist/base-skills/notis-spaces/.notis-base-skill.json +1 -0
- package/dist/base-skills/notis-spaces/SKILL.md +218 -0
- package/dist/base-skills/notis-spaces/references/architecture.md +116 -0
- package/dist/base-skills/notis-spaces/references/context.md +81 -0
- package/dist/base-skills/notis-spaces/references/design.md +141 -0
- package/dist/base-skills/notis-spaces/references/fixtures.md +29 -0
- package/dist/base-skills/notis-spaces/references/memory.md +58 -0
- package/dist/base-skills/notis-spaces/references/reading.md +73 -0
- package/dist/base-skills/notis-spaces/references/release.md +120 -0
- package/dist/base-skills/notis-spaces/references/reports.md +77 -0
- package/dist/base-skills/notis-spaces/references/sdk.md +273 -0
- package/dist/base-skills/notis-spaces/references/store.md +118 -0
- package/dist/base-skills/notis-spaces/references/troubleshooting.md +28 -0
- package/dist/base-skills/notis-spaces/references/views.md +229 -0
- package/{template/packages → dist}/sdk/package.json +9 -10
- package/dist/sdk/src/agentContext.ts +36 -0
- package/dist/sdk/src/bundleModule.ts +175 -0
- package/dist/sdk/src/components/Dialog.tsx +103 -0
- package/dist/sdk/src/components/DocumentEditor.tsx +113 -0
- package/dist/sdk/src/components/DocumentPage.tsx +184 -0
- package/{template/packages → dist}/sdk/src/components/Markdown.tsx +9 -3
- package/{template/packages → dist}/sdk/src/components/MarkdownEditor.tsx +26 -5
- package/{template/packages → dist}/sdk/src/components/MultiSelectActionBar.tsx +95 -12
- package/{template/packages → dist}/sdk/src/components/MultiSelectCheckbox.tsx +4 -2
- package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
- package/{template/packages → dist}/sdk/src/components/NotisSelectionBoundary.tsx +9 -5
- package/dist/sdk/src/components/RecordComponents.tsx +111 -0
- package/dist/sdk/src/components/Skeleton.tsx +38 -0
- package/dist/sdk/src/config.ts +31 -0
- package/{template/packages → dist}/sdk/src/documents.ts +21 -6
- package/dist/sdk/src/hooks/useAction.ts +28 -0
- package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
- package/{template/packages → dist}/sdk/src/hooks/useBackend.ts +3 -2
- package/dist/sdk/src/hooks/useCloudComputer.ts +94 -0
- package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
- package/{template/packages → dist}/sdk/src/hooks/useCollectionInteractions.ts +157 -147
- package/dist/sdk/src/hooks/useDatabaseSchema.ts +50 -0
- package/{template/packages → dist}/sdk/src/hooks/useDatabaseSubscription.ts +2 -2
- package/dist/sdk/src/hooks/useDocument.ts +51 -0
- package/{template/packages → dist}/sdk/src/hooks/useDocuments.ts +25 -59
- package/{template/packages → dist}/sdk/src/hooks/useHandover.ts +7 -7
- package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/dist/sdk/src/hooks/useMarqueeSelection.ts +194 -0
- package/{template/packages → dist}/sdk/src/hooks/useMultiSelect.ts +2 -8
- package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
- package/dist/sdk/src/hooks/useNativeDocuments.ts +143 -0
- package/{template/packages → dist}/sdk/src/hooks/useNotis.ts +6 -1
- package/dist/sdk/src/hooks/useNotisNavigation.ts +70 -0
- package/dist/sdk/src/hooks/useQuery.ts +81 -0
- package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
- package/dist/sdk/src/hooks/useTopBarSearch.ts +102 -0
- package/dist/sdk/src/hooks/useTopBarTitleLeaf.ts +75 -0
- package/{template/packages → dist}/sdk/src/hooks/useUpsertDocument.ts +1 -1
- package/dist/sdk/src/hooks/useViewParams.ts +195 -0
- package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
- package/{template/packages → dist}/sdk/src/index.ts +64 -1
- package/{template/packages → dist}/sdk/src/interactions/actions.ts +14 -1
- package/{template/packages → dist}/sdk/src/interactions/shortcuts.tsx +129 -23
- package/dist/sdk/src/interactions/visibility.ts +13 -0
- package/{template/packages → dist}/sdk/src/interactions.ts +5 -1
- package/dist/sdk/src/nativeData.ts +39 -0
- package/{template/packages → dist}/sdk/src/provider.tsx +9 -4
- package/dist/sdk/src/queryCache.ts +292 -0
- package/dist/sdk/src/queryErrors.ts +51 -0
- package/dist/sdk/src/runtime.ts +675 -0
- package/dist/sdk/src/runtimeModules.ts +33 -0
- package/dist/sdk/src/space.ts +442 -0
- package/dist/sdk/src/standardViewParams.ts +36 -0
- package/{template/packages → dist}/sdk/src/styles.css +81 -16
- package/dist/sdk/src/tailwind.ts +66 -0
- package/dist/sdk/src/ui/charts/RenderChartData.tsx +25 -0
- package/dist/sdk/src/ui/engine/core.ts +392 -0
- package/dist/sdk/src/ui/engine/dom.ts +183 -0
- package/dist/sdk/src/ui/engine/hooks.ts +195 -0
- package/dist/sdk/src/ui/engine/instance.ts +63 -0
- package/dist/sdk/src/ui/engine/keycapContext.ts +2 -0
- package/dist/sdk/src/ui/engine/registry.ts +262 -0
- package/dist/sdk/src/ui/engine/types.ts +62 -0
- package/dist/sdk/src/ui/icons.tsx +91 -0
- package/dist/sdk/src/ui/util.ts +83 -0
- package/dist/sdk/src/viewPeriod.ts +26 -0
- package/dist/sdk/src/vite.ts +92 -0
- package/dist/sdk/src/words/actions.ts +295 -0
- package/dist/sdk/src/words/clashes.ts +158 -0
- package/dist/sdk/src/words/format.ts +136 -0
- package/dist/sdk/src/words/index.ts +13 -0
- package/dist/sdk/src/words/keymap.ts +72 -0
- package/dist/sdk/src/words/keys.ts +297 -0
- package/dist/sdk/src/words/shell.ts +46 -0
- package/dist/sdk/src/words/types.ts +65 -0
- package/dist/skill-sync/index.js +832 -151
- package/dist/skill-sync/index.js.map +4 -4
- package/dist/skill-sync-worker.mjs +3941 -0
- package/dist/space-harness/host.js +1460 -0
- package/dist/space-sdk/dist/chunk-ZSN57ATH.js +5 -0
- package/dist/space-sdk/dist/index.js +15 -0
- package/dist/space-sdk/dist/ui.js +18 -0
- package/dist/space-sdk/package.json +33 -0
- package/dist/space-sdk/src/agentContext.d.ts +36 -0
- package/dist/space-sdk/src/components/Dialog.d.ts +20 -0
- package/dist/space-sdk/src/components/DocumentEditor.d.ts +46 -0
- package/dist/space-sdk/src/components/DocumentPage.d.ts +53 -0
- package/dist/space-sdk/src/components/Markdown.d.ts +18 -0
- package/dist/space-sdk/src/components/MarkdownEditor.d.ts +11 -0
- package/dist/space-sdk/src/components/MultiSelectActionBar.d.ts +31 -0
- package/dist/space-sdk/src/components/MultiSelectCheckbox.d.ts +14 -0
- package/dist/space-sdk/src/components/MultiSelectDragOverlay.d.ts +8 -0
- package/dist/space-sdk/src/components/NotisCommentBoundary.d.ts +36 -0
- package/dist/space-sdk/src/components/NotisSelectionBoundary.d.ts +16 -0
- package/dist/space-sdk/src/components/RecordComponents.d.ts +64 -0
- package/dist/space-sdk/src/components/ShortcutHints.d.ts +12 -0
- package/dist/space-sdk/src/components/Skeleton.d.ts +23 -0
- package/dist/space-sdk/src/config.ts +31 -0
- package/dist/space-sdk/src/documents.d.ts +59 -0
- package/dist/space-sdk/src/hooks/useAction.d.ts +8 -0
- package/dist/space-sdk/src/hooks/useActiveResource.d.ts +3 -0
- package/dist/space-sdk/src/hooks/useAgentContext.d.ts +7 -0
- package/dist/space-sdk/src/hooks/useBackend.d.ts +23 -0
- package/dist/space-sdk/src/hooks/useCloudComputer.d.ts +45 -0
- package/dist/space-sdk/src/hooks/useCloudComputerShell.d.ts +49 -0
- package/dist/space-sdk/src/hooks/useCollectionInteractions.d.ts +118 -0
- package/dist/space-sdk/src/hooks/useDatabaseSchema.d.ts +14 -0
- package/dist/space-sdk/src/hooks/useDatabaseSubscription.d.ts +26 -0
- package/dist/space-sdk/src/hooks/useDocument.d.ts +22 -0
- package/dist/space-sdk/src/hooks/useDocuments.d.ts +31 -0
- package/dist/space-sdk/src/hooks/useHandover.d.ts +43 -0
- package/dist/space-sdk/src/hooks/useLongPressSelection.d.ts +9 -0
- package/dist/space-sdk/src/hooks/useMarqueeSelection.d.ts +57 -0
- package/dist/space-sdk/src/hooks/useMultiSelect.d.ts +51 -0
- package/dist/space-sdk/src/hooks/useNativeDocumentBody.d.ts +24 -0
- package/dist/space-sdk/src/hooks/useNativeDocuments.d.ts +55 -0
- package/dist/space-sdk/src/hooks/useNotis.d.ts +25 -0
- package/dist/space-sdk/src/hooks/useNotisNavigation.d.ts +33 -0
- package/dist/space-sdk/src/hooks/useQuery.d.ts +22 -0
- package/dist/space-sdk/src/hooks/useTool.d.ts +20 -0
- package/dist/space-sdk/src/hooks/useToolQuery.d.ts +3 -0
- package/dist/space-sdk/src/hooks/useTools.d.ts +15 -0
- package/dist/space-sdk/src/hooks/useTopBarSearch.d.ts +48 -0
- package/dist/space-sdk/src/hooks/useTopBarTitleLeaf.d.ts +24 -0
- package/dist/space-sdk/src/hooks/useUpsertDocument.d.ts +27 -0
- package/dist/space-sdk/src/hooks/useViewParams.d.ts +76 -0
- package/dist/space-sdk/src/hooks/useViewerReads.d.ts +38 -0
- package/dist/space-sdk/src/index.d.ts +98 -0
- package/dist/space-sdk/src/interactions/actions.d.ts +33 -0
- package/dist/space-sdk/src/interactions/shortcuts.d.ts +42 -0
- package/dist/space-sdk/src/interactions/visibility.d.ts +2 -0
- package/dist/space-sdk/src/nativeData.d.ts +66 -0
- package/dist/space-sdk/src/provider.d.ts +17 -0
- package/dist/space-sdk/src/queryCache.d.ts +45 -0
- package/dist/space-sdk/src/queryErrors.ts +51 -0
- package/dist/space-sdk/src/runtime.d.ts +615 -0
- package/dist/space-sdk/src/runtimeModules.ts +33 -0
- package/dist/space-sdk/src/space.ts +442 -0
- package/dist/space-sdk/src/standardViewParams.d.ts +15 -0
- package/dist/space-sdk/src/tailwind-candidates.d.ts +3418 -0
- package/dist/space-sdk/src/tailwind.ts +66 -0
- package/dist/space-sdk/src/ui/actions/ActionToast.d.ts +15 -0
- package/dist/space-sdk/src/ui/actions/actionWords.d.ts +16 -0
- package/dist/space-sdk/src/ui/actions/index.d.ts +7 -0
- package/dist/space-sdk/src/ui/actions/useActions.d.ts +69 -0
- package/dist/space-sdk/src/ui/actions/useToast.d.ts +41 -0
- package/dist/space-sdk/src/ui/blocks/Tabs.d.ts +139 -0
- package/dist/space-sdk/src/ui/blocks/blocks.d.ts +217 -0
- package/dist/space-sdk/src/ui/blocks/index.d.ts +4 -0
- package/dist/space-sdk/src/ui/board/BoardView.d.ts +36 -0
- package/dist/space-sdk/src/ui/board/boardModel.d.ts +46 -0
- package/dist/space-sdk/src/ui/board/boardWords.d.ts +9 -0
- package/dist/space-sdk/src/ui/board/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/calendar/CalendarView.d.ts +49 -0
- package/dist/space-sdk/src/ui/calendar/DayPicker.d.ts +23 -0
- package/dist/space-sdk/src/ui/calendar/calendarMath.d.ts +81 -0
- package/dist/space-sdk/src/ui/charts/Chart.d.ts +203 -0
- package/dist/space-sdk/src/ui/charts/ChartTable.d.ts +37 -0
- package/dist/space-sdk/src/ui/charts/RenderChartData.d.ts +16 -0
- package/dist/space-sdk/src/ui/charts/StatRow.d.ts +59 -0
- package/dist/space-sdk/src/ui/charts/chartData.d.ts +84 -0
- package/dist/space-sdk/src/ui/comments/CommentPane.d.ts +58 -0
- package/dist/space-sdk/src/ui/comments/commentModel.d.ts +157 -0
- package/dist/space-sdk/src/ui/comments/commentWords.d.ts +60 -0
- package/dist/space-sdk/src/ui/comments/index.d.ts +4 -0
- package/dist/space-sdk/src/ui/comments/useComments.d.ts +146 -0
- package/dist/space-sdk/src/ui/engine/HelpSheet.d.ts +12 -0
- package/dist/space-sdk/src/ui/engine/KeyboardEngine.d.ts +27 -0
- package/dist/space-sdk/src/ui/engine/core.d.ts +73 -0
- package/dist/space-sdk/src/ui/engine/hooks.d.ts +64 -0
- package/dist/space-sdk/src/ui/engine/index.d.ts +14 -0
- package/dist/space-sdk/src/ui/engine/instance.d.ts +21 -0
- package/dist/space-sdk/src/ui/engine/keycapContext.d.ts +2 -0
- package/dist/space-sdk/src/ui/engine/registry.d.ts +77 -0
- package/dist/space-sdk/src/ui/engine/step.d.ts +11 -0
- package/dist/space-sdk/src/ui/engine/types.d.ts +54 -0
- package/dist/space-sdk/src/ui/fields/fieldWords.d.ts +56 -0
- package/dist/space-sdk/src/ui/fields/fields.d.ts +162 -0
- package/dist/space-sdk/src/ui/fields/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/fields/useFieldDraft.d.ts +22 -0
- package/dist/space-sdk/src/ui/gallery/AttachmentGallery.d.ts +53 -0
- package/dist/space-sdk/src/ui/gallery/ImageViewer.d.ts +27 -0
- package/dist/space-sdk/src/ui/gallery/galleryWords.d.ts +95 -0
- package/dist/space-sdk/src/ui/gallery/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/icons.d.ts +13 -0
- package/dist/space-sdk/src/ui/index.d.ts +54 -0
- package/dist/space-sdk/src/ui/keycap/Keycap.d.ts +22 -0
- package/dist/space-sdk/src/ui/keycap/index.d.ts +2 -0
- package/dist/space-sdk/src/ui/layout/Panels.d.ts +50 -0
- package/dist/space-sdk/src/ui/layout/chatPanelEdge.d.ts +77 -0
- package/dist/space-sdk/src/ui/layout/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/layout/panelMath.d.ts +91 -0
- package/dist/space-sdk/src/ui/list/ListReader.d.ts +380 -0
- package/dist/space-sdk/src/ui/list/Row.d.ts +145 -0
- package/dist/space-sdk/src/ui/list/SelectionBar.d.ts +65 -0
- package/dist/space-sdk/src/ui/list/index.d.ts +14 -0
- package/dist/space-sdk/src/ui/list/rowState.d.ts +22 -0
- package/dist/space-sdk/src/ui/list/useListKeys.d.ts +89 -0
- package/dist/space-sdk/src/ui/list/useReorder.d.ts +86 -0
- package/dist/space-sdk/src/ui/list/useSelection.d.ts +90 -0
- package/dist/space-sdk/src/ui/open/OpenAsMenu.d.ts +15 -0
- package/dist/space-sdk/src/ui/open/index.d.ts +6 -0
- package/dist/space-sdk/src/ui/open/openModeStore.d.ts +39 -0
- package/dist/space-sdk/src/ui/open/useOpenMode.d.ts +17 -0
- package/dist/space-sdk/src/ui/overlay/Menu.d.ts +88 -0
- package/dist/space-sdk/src/ui/overlay/Popover.d.ts +28 -0
- package/dist/space-sdk/src/ui/overlay/Tooltip.d.ts +19 -0
- package/dist/space-sdk/src/ui/overlay/index.d.ts +9 -0
- package/dist/space-sdk/src/ui/overlay/layerRoot.d.ts +24 -0
- package/dist/space-sdk/src/ui/overlay/position.d.ts +31 -0
- package/dist/space-sdk/src/ui/period/PeriodPicker.d.ts +30 -0
- package/dist/space-sdk/src/ui/period/dateFormat.d.ts +50 -0
- package/dist/space-sdk/src/ui/period/dateWords.d.ts +88 -0
- package/dist/space-sdk/src/ui/period/periods.d.ts +70 -0
- package/dist/space-sdk/src/ui/period/usePeriod.d.ts +23 -0
- package/dist/space-sdk/src/ui/present/index.d.ts +3 -0
- package/dist/space-sdk/src/ui/present/presentWords.d.ts +27 -0
- package/dist/space-sdk/src/ui/present/screens.d.ts +36 -0
- package/dist/space-sdk/src/ui/present/useAudienceScreen.d.ts +38 -0
- package/dist/space-sdk/src/ui/styles.d.ts +21 -0
- package/dist/space-sdk/src/ui/timeline/TimelineView.d.ts +44 -0
- package/dist/space-sdk/src/ui/timeline/timelineMath.d.ts +123 -0
- package/dist/space-sdk/src/viewPeriod.d.ts +8 -0
- package/dist/space-sdk/src/vite.ts +92 -0
- package/dist/space-sdk/src/words/actions.ts +295 -0
- package/dist/space-sdk/src/words/clashes.ts +158 -0
- package/dist/space-sdk/src/words/format.ts +136 -0
- package/dist/space-sdk/src/words/index.ts +13 -0
- package/dist/space-sdk/src/words/keymap.ts +72 -0
- package/dist/space-sdk/src/words/keys.ts +297 -0
- package/dist/space-sdk/src/words/shell.ts +46 -0
- package/dist/space-sdk/src/words/types.ts +65 -0
- package/dist/view-renderer/dist/host.css +1 -0
- package/dist/view-renderer/dist/host.js +2374 -0
- package/dist/view-renderer/dist/theme.css +73 -0
- package/dist/view-renderer/src/dom.js +212 -0
- package/dist/view-renderer/src/errors.js +8 -0
- package/dist/view-renderer/src/host.tsx +250 -0
- package/dist/view-renderer/src/index.js +4 -0
- package/dist/view-renderer/src/png.js +213 -0
- package/dist/view-renderer/src/pool.js +110 -0
- package/dist/view-renderer/src/remote-broker.js +59 -0
- package/dist/view-renderer/src/renderer.js +271 -0
- package/package.json +21 -11
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +63 -12
- package/skills/notis-onboarding/BRIEF.md +14 -9
- package/skills/notis-query/cli.md +1 -1
- package/skills/notis-spaces/cli.md +540 -0
- package/src/cli.js +8 -8
- package/src/command-specs/agents.js +121 -8
- package/src/command-specs/auth.js +3 -3
- package/src/command-specs/diagnostics.js +97 -20
- package/src/command-specs/documents.js +158 -0
- package/src/command-specs/files.js +29 -0
- package/src/command-specs/handover.js +331 -79
- package/src/command-specs/helpers.js +50 -40
- package/src/command-specs/index.js +14 -5
- package/src/command-specs/meta.js +38 -9
- package/src/command-specs/onboarding.js +1 -1
- package/src/command-specs/profile.js +42 -1
- package/src/command-specs/skills.js +314 -9
- package/src/command-specs/smoke.js +16 -4
- package/src/command-specs/spaces.js +783 -0
- package/src/command-specs/tools.js +118 -21
- package/src/command-specs/version-history.js +20 -0
- package/src/command-specs/views.js +69 -0
- package/src/runtime/agent-browser.js +178 -267
- package/src/runtime/agent-setup.js +172 -13
- package/src/runtime/app-boundary-validator.js +338 -2
- package/src/runtime/app-data-literals.js +82 -0
- package/src/runtime/app-platform.js +750 -1568
- package/src/runtime/auth-recovery.js +17 -0
- package/src/runtime/base-skills.d.ts +11 -0
- package/src/runtime/base-skills.js +100 -3
- package/src/runtime/computer-identity.d.ts +30 -0
- package/src/runtime/computer-identity.js +151 -0
- package/src/runtime/config-lock.js +135 -0
- package/src/runtime/confined-space-build.js +74 -0
- package/src/runtime/confined-space-verify.js +49 -0
- package/src/runtime/delegated-context.js +5 -21
- package/src/runtime/native-record-files.js +47 -0
- package/src/runtime/oauth.js +342 -31
- package/src/runtime/profiles.js +19 -112
- package/src/runtime/retired-sdk-files.js +24 -0
- package/src/runtime/retry.js +150 -0
- package/src/runtime/skill-sync/cloud-client.ts +33 -8
- package/src/runtime/skill-sync/index.ts +443 -106
- package/src/runtime/skill-sync/local-scanner.ts +248 -32
- package/src/runtime/skill-sync/native-identity.ts +102 -0
- package/src/runtime/skill-sync/native-plan.ts +70 -0
- package/src/runtime/skill-sync/symlink-manager.ts +66 -16
- package/src/runtime/skill-sync/sync-plan.ts +34 -2
- package/src/runtime/skill-sync/types.ts +54 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +4 -4
- package/src/runtime/skill-sync-service.js +109 -0
- package/src/runtime/source-root.js +32 -0
- package/src/runtime/space-collection-scaffold.js +107 -0
- package/src/runtime/space-database-structures.js +229 -0
- package/src/runtime/space-fixture-broker.js +68 -0
- package/src/runtime/space-lock.js +337 -0
- package/src/runtime/space-platform.js +133 -0
- package/src/runtime/space-source.js +290 -0
- package/src/runtime/space-test-server.js +354 -0
- package/src/runtime/space-verification.js +94 -0
- package/src/runtime/space-verify-browser-worker.js +51 -0
- package/src/runtime/space-view-manifest.js +368 -0
- package/src/runtime/sync-skills.d.ts +5 -1
- package/src/runtime/sync-skills.js +11 -2
- package/src/runtime/tool-confirmation.js +138 -0
- package/src/runtime/transport.js +118 -24
- package/src/runtime/upsell.js +219 -0
- package/src/runtime/view-render.js +49 -0
- package/src/skill-sync-worker-entry.js +2 -0
- package/src/skill-sync-worker.js +114 -0
- package/src/space-harness-host.ts +122 -0
- package/template/.harness/index.html.tmpl +9 -352
- package/template/app/globals.css +29 -3
- package/template/app/layout.tsx +2 -6
- package/template/components/ui/badge.tsx +7 -4
- package/template/components/ui/button.tsx +1 -1
- package/template/components/ui/native-select.tsx +24 -0
- package/template/notis.config.ts +4 -36
- package/template/package-lock.json +1800 -1867
- package/template/package.json +16 -14
- package/template/postcss.config.mjs +1 -1
- package/template/tailwind.config.ts +1 -6
- package/template/tsconfig.json +1 -0
- package/template/vite.config.ts +2 -2
- package/dist/base-skills/notis-apps/SKILL.md +0 -613
- package/skills/notis-apps/cli.md +0 -285
- package/src/command-specs/apps.js +0 -3000
- package/src/runtime/app-changelog.js +0 -79
- package/src/runtime/app-dev-consumers.js +0 -154
- package/src/runtime/app-dev-host-lock.js +0 -80
- package/src/runtime/app-dev-process-identity.js +0 -111
- package/src/runtime/app-dev-roots.js +0 -284
- package/src/runtime/app-dev-server.js +0 -1140
- package/src/runtime/app-dev-sessions.js +0 -185
- package/src/runtime/app-registry-scaffolds.js +0 -367
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/cli-mode.generated.js +0 -5
- package/src/runtime/cli-mode.js +0 -34
- package/src/runtime/store-screenshot.js +0 -143
- package/template/CHANGELOG.md +0 -5
- package/template/app/page.tsx +0 -60
- package/template/components/ui/card.tsx +0 -56
- package/template/packages/sdk/src/components/DocumentEditor.tsx +0 -103
- package/template/packages/sdk/src/config.ts +0 -259
- package/template/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/template/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/template/packages/sdk/src/hooks/useNotisNavigation.ts +0 -52
- package/template/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/template/packages/sdk/src/runtime.ts +0 -439
- package/template/packages/sdk/src/vite.ts +0 -56
- package/template/packages/sdk/tsconfig.json +0 -15
- /package/{template/packages → dist}/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/components/ShortcutHints.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useActiveResource.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useTool.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useTools.ts +0 -0
- /package/{template/packages → dist}/sdk/src/ui.ts +0 -0
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
1
4
|
import type {
|
|
5
|
+
AgentTargets,
|
|
2
6
|
LocalSkill,
|
|
3
7
|
NotisSyncState,
|
|
4
8
|
SkillSyncFailure,
|
|
5
9
|
SyncPullResponse,
|
|
6
10
|
SyncSettings,
|
|
11
|
+
SyncedSkill,
|
|
7
12
|
} from "./types";
|
|
8
13
|
import { normalizeAgentTargets } from "./types";
|
|
14
|
+
import { assertNativePull, checkedNativeReference, isNativeSkill, nativeState } from './native-identity';
|
|
15
|
+
import { bindNativeFolders, hasNativeFolderHistory, nativeLinkRows, nativeLocalReady, pendingLegacyUpgrade, previousNativeState, retainMissingNativeState } from './native-plan';
|
|
9
16
|
import {
|
|
10
17
|
downloadSkillBundle,
|
|
11
18
|
fetchSyncSettings,
|
|
@@ -30,8 +37,9 @@ import {
|
|
|
30
37
|
syncSymlinks,
|
|
31
38
|
type DeletedAgentSymlink,
|
|
32
39
|
} from "./symlink-manager";
|
|
33
|
-
import { getPushCandidates } from "./sync-plan";
|
|
40
|
+
import { getPushCandidates, selectCloudSkillsToApply } from "./sync-plan";
|
|
34
41
|
import { writeCloudSkillWithBundleFallback } from "./write-cloud-skill";
|
|
42
|
+
export { fetchSyncSettings } from './cloud-client';
|
|
35
43
|
|
|
36
44
|
export interface RunSkillSyncResult {
|
|
37
45
|
syncEnabled: boolean;
|
|
@@ -49,15 +57,22 @@ export interface RunSkillSyncResult {
|
|
|
49
57
|
/** Skills the server rejected (e.g. an invalid SKILL.md description). The rest of
|
|
50
58
|
* the batch still syncs; these are surfaced so the user knows what to fix. */
|
|
51
59
|
failedPushes: SkillSyncFailure[];
|
|
60
|
+
failedLinks?: SkillSyncFailure[];
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
export interface RunSkillSyncOptions {
|
|
55
64
|
/** Electron's scheduled invocation honors the account preference. A manual
|
|
56
65
|
* CLI sync always runs, even when automatic Desktop refresh is disabled. */
|
|
57
66
|
honorSyncEnabled?: boolean;
|
|
67
|
+
/** All account paths, scanner roots and agent links use this explicit home. */
|
|
68
|
+
syncHome?: string;
|
|
69
|
+
/** False when another account is the CLI default: sync this account's own
|
|
70
|
+
* folder only and leave the agent folders (Claude Code, Codex, Cursor) to the
|
|
71
|
+
* default's skills. Defaults to true. */
|
|
72
|
+
linkAgentFolders?: boolean;
|
|
58
73
|
}
|
|
59
74
|
|
|
60
|
-
const BASE_SKILL_NAMES = new Set(['notis-
|
|
75
|
+
const BASE_SKILL_NAMES = new Set(['notis-spaces', 'notis-query', 'notis-cli']);
|
|
61
76
|
|
|
62
77
|
function withoutBaseSkills(pullResponse: SyncPullResponse): SyncPullResponse {
|
|
63
78
|
return {
|
|
@@ -76,14 +91,21 @@ function withoutBaseSkillState(state: NotisSyncState): NotisSyncState {
|
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
export interface MaterializeCloudSkillsResult {
|
|
94
|
+
/** Cloud skills verified present in the final scoped disk scan. */
|
|
95
|
+
materializedSkillNames: string[];
|
|
79
96
|
pulled: number;
|
|
80
97
|
downloaded: number;
|
|
81
98
|
deleted: number;
|
|
82
99
|
removed: number;
|
|
83
100
|
lastSyncedAt: string | null;
|
|
101
|
+
failedLinks?: SkillSyncFailure[];
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
export interface MaterializeCloudSkillsOptions {
|
|
105
|
+
/** Server-verified Notis identity, when Desktop's auth subject differs. */
|
|
106
|
+
canonicalUserId?: string;
|
|
107
|
+
/** Previously authenticated settings must agree with the pull before I/O. */
|
|
108
|
+
syncSettings?: SyncSettings;
|
|
87
109
|
/** Re-link only these cloud skills without removing or adopting other links. */
|
|
88
110
|
relinkSkillNames?: readonly string[];
|
|
89
111
|
}
|
|
@@ -125,6 +147,74 @@ const DEFAULT_RUN_SKILL_SYNC_DEPS: RunSkillSyncDependencies = {
|
|
|
125
147
|
removeForeignAccountSymlinks,
|
|
126
148
|
updateAgentTargets,
|
|
127
149
|
};
|
|
150
|
+
|
|
151
|
+
// Another account is the CLI default and owns the agent folders: this account
|
|
152
|
+
// syncs its own folder without linking, unlinking or adopting anything there.
|
|
153
|
+
const MIRROR_ONLY_DEPS: Pick<
|
|
154
|
+
RunSkillSyncDependencies,
|
|
155
|
+
'removeForeignAccountSymlinks' | 'removeAllSymlinksForSkill' | 'syncSymlinks'
|
|
156
|
+
> = {
|
|
157
|
+
removeForeignAccountSymlinks: async () => 0,
|
|
158
|
+
removeAllSymlinksForSkill: async () => 0,
|
|
159
|
+
syncSymlinks: async () => ({ linked: 0, removed: 0, skipped: 0, verifiedAgentLinks: {} }),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The account linking now owns the agent folders, so the links the other
|
|
164
|
+
* accounts on this computer last verified are gone. Forget them: otherwise that
|
|
165
|
+
* account's next linking sync would read each missing link as the user removing
|
|
166
|
+
* the skill and deactivate it.
|
|
167
|
+
*/
|
|
168
|
+
async function forgetOtherAccountsAgentLinks(
|
|
169
|
+
paths: ReturnType<typeof getSkillSyncPathsForUser>,
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
const usersDir = dirname(dirname(paths.syncStatePath));
|
|
172
|
+
let entries: import('node:fs').Dirent[];
|
|
173
|
+
try {
|
|
174
|
+
entries = await fs.readdir(usersDir, { withFileTypes: true });
|
|
175
|
+
} catch {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (!entry.isDirectory()) continue;
|
|
180
|
+
const statePath = join(usersDir, entry.name, '.notis-sync.json');
|
|
181
|
+
if (statePath === paths.syncStatePath) continue;
|
|
182
|
+
let state: { skills?: Record<string, { verifiedAgentLinks?: unknown }> } | null = null;
|
|
183
|
+
try {
|
|
184
|
+
state = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
185
|
+
} catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (!state || typeof state !== 'object' || !state.skills || typeof state.skills !== 'object') continue;
|
|
189
|
+
let changed = false;
|
|
190
|
+
for (const skill of Object.values(state.skills)) {
|
|
191
|
+
if (skill && typeof skill === 'object' && 'verifiedAgentLinks' in skill) {
|
|
192
|
+
delete skill.verifiedAgentLinks;
|
|
193
|
+
changed = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!changed) continue;
|
|
197
|
+
const temporaryPath = `${statePath}.${process.pid}.${Date.now()}.tmp`;
|
|
198
|
+
await fs.writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
199
|
+
await fs.rename(temporaryPath, statePath);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function isolatedLinkOptions(paths: ReturnType<typeof getSkillSyncPathsForUser>) {
|
|
204
|
+
if (!paths.isolatedNamespace && !paths.homeDir) return undefined;
|
|
205
|
+
const root = paths.isolatedNamespace
|
|
206
|
+
? join(paths.syncRoot, 'environments', paths.isolatedNamespace, 'agents') : paths.homeDir!;
|
|
207
|
+
return { agentSkillDirs: { notis: paths.skillsDir, claude_code: join(root, '.claude', 'skills'),
|
|
208
|
+
cursor: join(root, '.cursor', 'skills'), codex: join(root, '.codex', 'skills') },
|
|
209
|
+
legacyGlobalSkillsDir: join(root, '.agents', 'skills') };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function assertSyncEnvironment(pull: SyncPullResponse, settings: SyncSettings): void {
|
|
213
|
+
if ((pull.sync_namespace || null) !== (settings.sync_namespace || null)
|
|
214
|
+
|| (settings.sync_scope !== undefined && pull.sync_scope !== settings.sync_scope)) {
|
|
215
|
+
throw new Error('Skill sync environment changed. Existing mirrors were retained.');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
128
218
|
function toSkillMap(skills: LocalSkill[]): Map<string, LocalSkill> {
|
|
129
219
|
return new Map(skills.map((skill) => [skill.name, skill]));
|
|
130
220
|
}
|
|
@@ -144,7 +234,17 @@ function decodeJwtSubject(jwt: string): string | null {
|
|
|
144
234
|
}
|
|
145
235
|
}
|
|
146
236
|
|
|
147
|
-
function
|
|
237
|
+
function cloudContentHash(skill: SyncPullResponse['skills'][number]): string {
|
|
238
|
+
return createHash('sha256').update(JSON.stringify({
|
|
239
|
+
md: skill.skill_md,
|
|
240
|
+
hash: skill.skill_folder_hash,
|
|
241
|
+
source: skill.skill_source_url,
|
|
242
|
+
files: skill.bundle_files?.slice().sort((a, b) => a.path.localeCompare(b.path)),
|
|
243
|
+
hydrationFailed: skill.bundle_hydration_failed === true,
|
|
244
|
+
})).digest('hex');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function shouldWriteCloudSkill(
|
|
148
248
|
cloudSkill: SyncPullResponse["skills"][number],
|
|
149
249
|
localSkills: Map<string, LocalSkill>,
|
|
150
250
|
previousState: NotisSyncState,
|
|
@@ -156,46 +256,150 @@ function shouldWriteCloudSkill(
|
|
|
156
256
|
return true;
|
|
157
257
|
}
|
|
158
258
|
|
|
259
|
+
if (isNativeSkill(cloudSkill)) {
|
|
260
|
+
if (localSkill.nativeScope && localSkill.nativeScope !== cloudSkill.native_scope) return false;
|
|
261
|
+
if (nativeLocalReady(cloudSkill, localSkill)) return false;
|
|
262
|
+
const nativePrevious = previousNativeState(previousState, cloudSkill);
|
|
263
|
+
return (localSkill.nativeFilesCurrent === true && localSkill.folderHash === localSkill.nativeBaseFolderHash)
|
|
264
|
+
|| (!localSkill.nativeReference && nativePrevious?.cloudId === cloudSkill.id
|
|
265
|
+
&& nativePrevious.folderHash === localSkill.folderHash);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const previous = previousState.skills[skillName];
|
|
269
|
+
if (previous?.folderHash === localSkill.folderHash
|
|
270
|
+
&& previous.cloudContentHash === cloudContentHash(cloudSkill)) return false;
|
|
271
|
+
|
|
272
|
+
// Without a stored content baseline (skills served through a signed source URL keep
|
|
273
|
+
// none, because that URL changes on every pull) the only other signal is
|
|
274
|
+
// `skill_folder_hash`, which the server does NOT always compute the way the local
|
|
275
|
+
// folder hash is computed. A bundle-entry hash can never equal the hash of the
|
|
276
|
+
// extracted directory, and comparing the two replaced the folder on every sync
|
|
277
|
+
// tick, deleting whatever a running skill had written inside it. Compare cloud against cloud instead: rewrite when the cloud
|
|
278
|
+
// revision we last applied has actually moved.
|
|
279
|
+
if (previous
|
|
280
|
+
&& previous.cloudContentHash === undefined
|
|
281
|
+
&& previous.cloudId === cloudSkill.id
|
|
282
|
+
&& previous.appliedCloudFolderHash !== undefined
|
|
283
|
+
&& previous.appliedCloudFolderHash === cloudHash
|
|
284
|
+
&& previous.cloudUpdatedAt === cloudSkill.updated_at
|
|
285
|
+
&& previous.folderHash === localSkill.folderHash) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
159
289
|
if (cloudSkill.source === "curated") {
|
|
160
290
|
return cloudHash ? cloudHash !== localSkill.folderHash : true;
|
|
161
291
|
}
|
|
162
292
|
|
|
163
|
-
const previous = previousState.skills[skillName];
|
|
164
293
|
const localChangedSinceLastSync =
|
|
165
294
|
!previous || previous.folderHash !== localSkill.folderHash;
|
|
166
295
|
return (
|
|
167
296
|
!localChangedSinceLastSync &&
|
|
168
|
-
Boolean(cloudHash) &&
|
|
169
|
-
|
|
297
|
+
((Boolean(cloudHash) && cloudHash !== localSkill.folderHash)
|
|
298
|
+
|| Boolean(previous?.cloudContentHash && previous.cloudContentHash !== cloudContentHash(cloudSkill)))
|
|
170
299
|
);
|
|
171
300
|
}
|
|
172
301
|
|
|
302
|
+
type AppliedCloudRevisions = Record<string, string | undefined>;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The cloud revision each folder's last successful write applied: this run's revision
|
|
306
|
+
* for the skills we just wrote, the previously recorded one otherwise. A failed write
|
|
307
|
+
* records nothing, so the next sync retries it.
|
|
308
|
+
*/
|
|
309
|
+
function collectAppliedCloudRevisions(
|
|
310
|
+
pullResponse: SyncPullResponse,
|
|
311
|
+
previousState: NotisSyncState,
|
|
312
|
+
writtenSkillNames: ReadonlySet<string>,
|
|
313
|
+
): AppliedCloudRevisions {
|
|
314
|
+
const applied: AppliedCloudRevisions = {};
|
|
315
|
+
for (const skill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
316
|
+
applied[skill.name] = writtenSkillNames.has(skill.name)
|
|
317
|
+
? skill.skill_folder_hash || ""
|
|
318
|
+
: previousState.skills[skill.name]?.appliedCloudFolderHash;
|
|
319
|
+
}
|
|
320
|
+
return applied;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The files each folder's last successful write put on disk: this run's list for the
|
|
325
|
+
* skills we just wrote, the previously recorded one otherwise. Missing means "no
|
|
326
|
+
* record", which keeps local files rather than deleting what cannot be attributed.
|
|
327
|
+
*/
|
|
328
|
+
function collectAppliedFiles(
|
|
329
|
+
pullResponse: SyncPullResponse,
|
|
330
|
+
previousState: NotisSyncState,
|
|
331
|
+
appliedFilesByName: Record<string, string[]>,
|
|
332
|
+
): Record<string, string[] | undefined> {
|
|
333
|
+
const applied: Record<string, string[] | undefined> = {};
|
|
334
|
+
for (const skill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
335
|
+
applied[skill.name] = appliedFilesByName[skill.name]
|
|
336
|
+
?? previousState.skills[skill.name]?.appliedFiles;
|
|
337
|
+
}
|
|
338
|
+
return applied;
|
|
339
|
+
}
|
|
340
|
+
|
|
173
341
|
function buildSyncState(
|
|
174
342
|
pullResponse: SyncPullResponse,
|
|
175
343
|
localSkills: LocalSkill[],
|
|
176
344
|
lastSyncedAt: string | null,
|
|
345
|
+
verifiedAgentLinks: Record<string, Partial<AgentTargets>> = {},
|
|
346
|
+
failedContentNames: ReadonlySet<string> = new Set(),
|
|
347
|
+
appliedRevisions: AppliedCloudRevisions = {},
|
|
348
|
+
appliedFiles: Record<string, string[] | undefined> = {},
|
|
349
|
+
previousState: NotisSyncState = { version: 1, lastSyncedAt: null, skills: {} },
|
|
177
350
|
): NotisSyncState {
|
|
178
351
|
const localSkillMap = toSkillMap(localSkills);
|
|
179
|
-
|
|
180
|
-
|
|
352
|
+
// The same row selection the writer used, so the state describes the revision that
|
|
353
|
+
// is actually on disk rather than whichever duplicate row the server listed last.
|
|
354
|
+
const skills = Object.fromEntries<SyncedSkill>(
|
|
355
|
+
selectCloudSkillsToApply(pullResponse.skills).map((skill): [string, SyncedSkill] => {
|
|
181
356
|
const localSkill = localSkillMap.get(skill.name);
|
|
357
|
+
const appliedCloudFolderHash = appliedRevisions[skill.name];
|
|
358
|
+
const appliedFileList = appliedFiles[skill.name];
|
|
359
|
+
if (pendingLegacyUpgrade(skill, localSkill, previousState)) {
|
|
360
|
+
// Keep the legacy baseline so the upgrade retries; never mark a usable folder unavailable.
|
|
361
|
+
const { nativeUnavailable: _unavailable, ...previous } = previousNativeState(previousState, skill)!;
|
|
362
|
+
return [skill.name, { ...previous, verifiedAgentLinks: verifiedAgentLinks[skill.name] ?? {} }];
|
|
363
|
+
}
|
|
364
|
+
if (isNativeSkill(skill) && !nativeLocalReady(skill, localSkill)) {
|
|
365
|
+
const previous = previousNativeState(previousState, skill);
|
|
366
|
+
return [skill.name, { ...(previous || {
|
|
367
|
+
cloudId: skill.id, folderHash: localSkill?.nativeBaseFolderHash || '',
|
|
368
|
+
agentTargets: normalizeAgentTargets(skill.agent_targets), syncedAt: lastSyncedAt || new Date().toISOString(),
|
|
369
|
+
...(localSkill?.nativeReference ? { nativeReference: localSkill.nativeReference, nativeScope: localSkill.nativeScope } : {}),
|
|
370
|
+
}), nativeUnavailable: true, verifiedAgentLinks: {} }];
|
|
371
|
+
}
|
|
182
372
|
return [
|
|
183
373
|
skill.name,
|
|
184
374
|
{
|
|
185
375
|
cloudId: skill.id,
|
|
376
|
+
...nativeState(skill),
|
|
186
377
|
folderHash: localSkill?.folderHash || skill.skill_folder_hash || "",
|
|
187
378
|
agentTargets: normalizeAgentTargets(skill.agent_targets),
|
|
379
|
+
verifiedAgentLinks: skill.status === "active" ? verifiedAgentLinks[skill.name] ?? {} : {},
|
|
380
|
+
cloudUpdatedAt: skill.updated_at,
|
|
381
|
+
...(!failedContentNames.has(skill.name) && !skill.skill_source_url
|
|
382
|
+
? { cloudContentHash: cloudContentHash(skill) } : {}),
|
|
383
|
+
...(appliedCloudFolderHash !== undefined ? { appliedCloudFolderHash } : {}),
|
|
384
|
+
...(appliedFileList !== undefined ? { appliedFiles: appliedFileList } : {}),
|
|
188
385
|
syncedAt: lastSyncedAt || new Date().toISOString(),
|
|
189
386
|
},
|
|
190
387
|
];
|
|
191
388
|
}),
|
|
192
389
|
);
|
|
193
390
|
|
|
194
|
-
|
|
391
|
+
for (const local of localSkills) {
|
|
392
|
+
if (!local.nativeReference || Object.values(skills).some(row => row.cloudId === local.cloudId)) continue;
|
|
393
|
+
skills[local.name] = { cloudId: local.cloudId!, nativeReference: local.nativeReference,
|
|
394
|
+
nativeScope: local.nativeScope, nativeUnavailable: true, verifiedAgentLinks: {},
|
|
395
|
+
folderHash: local.nativeBaseFolderHash || '', agentTargets: normalizeAgentTargets(),
|
|
396
|
+
syncedAt: lastSyncedAt || new Date().toISOString() };
|
|
397
|
+
}
|
|
398
|
+
return retainMissingNativeState(previousState, {
|
|
195
399
|
version: 1,
|
|
196
400
|
lastSyncedAt,
|
|
197
401
|
skills,
|
|
198
|
-
};
|
|
402
|
+
});
|
|
199
403
|
}
|
|
200
404
|
|
|
201
405
|
function buildLocalSymlinkCandidates(
|
|
@@ -208,6 +412,10 @@ function buildLocalSymlinkCandidates(
|
|
|
208
412
|
);
|
|
209
413
|
const localOnlySkills = localSkills
|
|
210
414
|
.filter((skill) => !cloudSkillNames.has(skill.name))
|
|
415
|
+
.filter(skill => !skill.nativeReference && !previousState.skills[skill.name]?.nativeReference
|
|
416
|
+
&& !previousState.skills[skill.name]?.nativeUnavailable
|
|
417
|
+
&& !hasNativeFolderHistory(previousState, skill.name)
|
|
418
|
+
&& !/^native-[a-f0-9-]{36}$/.test(skill.name))
|
|
211
419
|
.map((skill) => {
|
|
212
420
|
const previous = previousState.skills[skill.name];
|
|
213
421
|
return {
|
|
@@ -222,7 +430,7 @@ function buildLocalSymlinkCandidates(
|
|
|
222
430
|
};
|
|
223
431
|
});
|
|
224
432
|
|
|
225
|
-
return [...pullResponse
|
|
433
|
+
return [...nativeLinkRows(pullResponse, localSkills, previousState), ...localOnlySkills];
|
|
226
434
|
}
|
|
227
435
|
|
|
228
436
|
function isEmptySyncState(state: NotisSyncState): boolean {
|
|
@@ -268,6 +476,10 @@ async function writePulledSkillsToScopedMirror(
|
|
|
268
476
|
RunSkillSyncDependencies,
|
|
269
477
|
"downloadSkillBundle" | "writeCloudSkillToDisk"
|
|
270
478
|
>,
|
|
479
|
+
failures: SkillSyncFailure[] = [],
|
|
480
|
+
writtenSkillNames: Set<string> = new Set(),
|
|
481
|
+
appliedFilesByName: Record<string, string[]> = {},
|
|
482
|
+
acknowledgedNative: ReadonlyMap<string, string> = new Map(),
|
|
271
483
|
): Promise<number> {
|
|
272
484
|
const localSkillMap = toSkillMap(localSkills);
|
|
273
485
|
const warnSkillSync = (message: string, error: unknown): void => {
|
|
@@ -275,20 +487,40 @@ async function writePulledSkillsToScopedMirror(
|
|
|
275
487
|
};
|
|
276
488
|
|
|
277
489
|
let downloaded = 0;
|
|
278
|
-
for (const cloudSkill of pullResponse.skills) {
|
|
279
|
-
|
|
490
|
+
for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
491
|
+
const local = localSkillMap.get(cloudSkill.name);
|
|
492
|
+
const acknowledged = isNativeSkill(cloudSkill) && local && acknowledgedNative.get(cloudSkill.id) === local.folderHash;
|
|
493
|
+
// An unchanged pre-native folder has no applied-file record, so the files it shares
|
|
494
|
+
// with the incoming native Skill are the previous sync's output, not local edits.
|
|
495
|
+
const legacyBaseline = Boolean(local && pendingLegacyUpgrade(cloudSkill, local, previousState)
|
|
496
|
+
&& previousNativeState(previousState, cloudSkill)?.folderHash === local.folderHash);
|
|
497
|
+
if (!acknowledged && !shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
|
|
498
|
+
if (isNativeSkill(cloudSkill) && !nativeLocalReady(cloudSkill, local)) {
|
|
499
|
+
failures.push({ name: cloudSkill.name, error: 'Local native Skill changes were retained; resolve the version or identity conflict before using them.' });
|
|
500
|
+
}
|
|
280
501
|
continue;
|
|
281
502
|
}
|
|
282
503
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
writeCloudSkillToDisk
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
504
|
+
const outcome = await writeCloudSkillWithBundleFallback(cloudSkill, {
|
|
505
|
+
downloadSkillBundle: deps.downloadSkillBundle,
|
|
506
|
+
writeCloudSkillToDisk: (skill, bundleBytes) =>
|
|
507
|
+
deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths, {
|
|
508
|
+
// What the last write put there. Everything else in the folder was
|
|
509
|
+
// produced locally and survives this one.
|
|
510
|
+
previouslyApplied: previousNativeState(previousState, skill)?.appliedFiles,
|
|
511
|
+
...(isNativeSkill(skill) && local ? { expectedFolderHash: local.folderHash } : {}),
|
|
512
|
+
...(legacyBaseline ? { legacyBaseline } : {}),
|
|
513
|
+
}),
|
|
514
|
+
onWarning: warnSkillSync,
|
|
515
|
+
});
|
|
516
|
+
if (outcome) {
|
|
291
517
|
downloaded += 1;
|
|
518
|
+
writtenSkillNames.add(cloudSkill.name);
|
|
519
|
+
if (typeof outcome === "object" && Array.isArray(outcome.appliedFiles)) {
|
|
520
|
+
appliedFilesByName[cloudSkill.name] = outcome.appliedFiles;
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
failures.push({ name: cloudSkill.name, error: "Skill content could not be downloaded or written; sync will retry" });
|
|
292
524
|
}
|
|
293
525
|
}
|
|
294
526
|
|
|
@@ -298,6 +530,8 @@ async function writePulledSkillsToScopedMirror(
|
|
|
298
530
|
function assertSkillsPullAuthorized(
|
|
299
531
|
pullResponse: SyncPullResponse,
|
|
300
532
|
): void {
|
|
533
|
+
if (!Array.isArray(pullResponse.skills)) throw new Error('Skill inventory is unavailable; existing files were retained.');
|
|
534
|
+
assertNativePull(pullResponse);
|
|
301
535
|
if (
|
|
302
536
|
pullResponse.entitlement_access?.code === "entitlement_upgrade_required"
|
|
303
537
|
&& pullResponse.entitlement_access.entitlement === "skills"
|
|
@@ -340,42 +574,99 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
340
574
|
);
|
|
341
575
|
}
|
|
342
576
|
|
|
343
|
-
|
|
344
|
-
const pullResponse = await deps.pullSkills(serverUrl, jwt);
|
|
577
|
+
let pullResponse = await deps.pullSkills(serverUrl, jwt);
|
|
345
578
|
assertSkillsPullAuthorized(pullResponse);
|
|
579
|
+
if (options.syncSettings) assertSyncEnvironment(pullResponse, options.syncSettings);
|
|
580
|
+
const syncPaths = getSkillSyncPathsForUser(options.canonicalUserId?.trim() || authUserId, undefined, pullResponse.sync_namespace);
|
|
346
581
|
const previousState = await deps.readSyncState(syncPaths);
|
|
347
582
|
|
|
348
583
|
const localSkills = await deps.scanLocalSkills(syncPaths);
|
|
584
|
+
pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
|
|
585
|
+
const failedDownloads: SkillSyncFailure[] = [];
|
|
586
|
+
const writtenSkillNames = new Set<string>();
|
|
587
|
+
const appliedFilesByName: Record<string, string[]> = {};
|
|
349
588
|
const downloaded = await writePulledSkillsToScopedMirror(
|
|
350
589
|
pullResponse,
|
|
351
590
|
localSkills,
|
|
352
591
|
previousState,
|
|
353
592
|
syncPaths,
|
|
354
593
|
deps,
|
|
594
|
+
failedDownloads,
|
|
595
|
+
writtenSkillNames,
|
|
596
|
+
appliedFilesByName,
|
|
355
597
|
);
|
|
356
598
|
const finalLocalSkills = await deps.scanLocalSkills(syncPaths);
|
|
357
599
|
const lastSyncedAt = pullResponse.last_synced_at || new Date().toISOString();
|
|
358
600
|
|
|
359
601
|
const relinkSkillNames = new Set(options.relinkSkillNames || []);
|
|
602
|
+
const failures = [...failedDownloads];
|
|
603
|
+
const verifiedLinks: Record<string, Partial<AgentTargets>> = {};
|
|
604
|
+
for (const skill of pullResponse.skills) {
|
|
605
|
+
const previous = previousState.skills[skill.name];
|
|
606
|
+
if (skill.updated_at && previous?.cloudId === skill.id && previous.cloudUpdatedAt === skill.updated_at) {
|
|
607
|
+
verifiedLinks[skill.name] = { ...previous.verifiedAgentLinks };
|
|
608
|
+
}
|
|
609
|
+
}
|
|
360
610
|
if (relinkSkillNames.size > 0) {
|
|
361
|
-
await deps.syncSymlinks(
|
|
362
|
-
pullResponse.
|
|
611
|
+
const relinked = await deps.syncSymlinks(
|
|
612
|
+
nativeLinkRows(pullResponse, finalLocalSkills, previousState).filter((skill) => relinkSkillNames.has(skill.name)),
|
|
363
613
|
syncPaths.skillsDir,
|
|
364
|
-
{ removeUndesired: false },
|
|
614
|
+
{ ...isolatedLinkOptions(syncPaths), removeUndesired: false },
|
|
365
615
|
);
|
|
616
|
+
failures.push(...(relinked.failures ?? []).filter(
|
|
617
|
+
(failure) => !failedDownloads.some((download) => download.name === failure.name),
|
|
618
|
+
));
|
|
619
|
+
for (const name of relinkSkillNames) {
|
|
620
|
+
verifiedLinks[name] = relinked.verifiedAgentLinks?.[name] ?? {};
|
|
621
|
+
}
|
|
366
622
|
}
|
|
623
|
+
for (const failure of failedDownloads) delete verifiedLinks[failure.name];
|
|
367
624
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
625
|
+
const materializedState = buildSyncState(
|
|
626
|
+
pullResponse, finalLocalSkills, lastSyncedAt, verifiedLinks,
|
|
627
|
+
new Set(failedDownloads.map(item => item.name)),
|
|
628
|
+
collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
|
|
629
|
+
collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
|
|
630
|
+
previousState,
|
|
371
631
|
);
|
|
632
|
+
// Pull-only refresh is not an upload acknowledgement. Keep content baselines
|
|
633
|
+
// unless we actually wrote cloud content, and retain cloud-missing entries so
|
|
634
|
+
// the next two-way sync can delete them instead of uploading them as new.
|
|
635
|
+
for (const [name, entry] of Object.entries(materializedState.skills)) {
|
|
636
|
+
if (writtenSkillNames.has(name)) continue;
|
|
637
|
+
const previous = previousState.skills[name];
|
|
638
|
+
if (previous) {
|
|
639
|
+
entry.folderHash = previous.folderHash;
|
|
640
|
+
entry.cloudContentHash = previous.cloudContentHash;
|
|
641
|
+
} else {
|
|
642
|
+
delete materializedState.skills[name];
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
materializedState.skills = { ...previousState.skills, ...materializedState.skills };
|
|
646
|
+
let removed = 0;
|
|
647
|
+
for (const [name, previous] of Object.entries(previousState.skills)) {
|
|
648
|
+
if (!previous.nativeReference && !previous.nativeUnavailable) continue;
|
|
649
|
+
const current = pullResponse.skills.find(skill => skill.id === previous.cloudId);
|
|
650
|
+
const local = current && finalLocalSkills.find(row => row.name === current.name);
|
|
651
|
+
if (!current || (!nativeLocalReady(current, local) && !pendingLegacyUpgrade(current, local, previousState))) {
|
|
652
|
+
removed += await deps.removeAllSymlinksForSkill(name, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
await deps.writeSyncState(materializedState, syncPaths);
|
|
372
656
|
|
|
373
657
|
return {
|
|
658
|
+
materializedSkillNames: pullResponse.skills
|
|
659
|
+
.filter((skill) => finalLocalSkills.some((local) => local.name === skill.name)
|
|
660
|
+
&& (!isNativeSkill(skill) || (skill.status === 'active' && skill.agent_targets?.notis !== false
|
|
661
|
+
&& nativeLocalReady(skill, finalLocalSkills.find(local => local.name === skill.name))))
|
|
662
|
+
&& !failedDownloads.some((failure) => failure.name === skill.name))
|
|
663
|
+
.map((skill) => skill.name),
|
|
374
664
|
pulled: pullResponse.skills.length,
|
|
375
665
|
downloaded,
|
|
376
666
|
deleted: 0,
|
|
377
|
-
removed
|
|
667
|
+
removed,
|
|
378
668
|
lastSyncedAt,
|
|
669
|
+
...(failures.length ? { failedLinks: failures } : {}),
|
|
379
670
|
};
|
|
380
671
|
}
|
|
381
672
|
|
|
@@ -397,6 +688,8 @@ async function deactivateDeletedAgentSkills(
|
|
|
397
688
|
RunSkillSyncDependencies,
|
|
398
689
|
"detectDeletedAgentSymlinks" | "updateAgentTargets" | "pullSkills"
|
|
399
690
|
>,
|
|
691
|
+
failures: SkillSyncFailure[],
|
|
692
|
+
linkOptions?: ReturnType<typeof isolatedLinkOptions>,
|
|
400
693
|
): Promise<number> {
|
|
401
694
|
// First sync (incl. legacy migration) has no reliable "we created this link" signal, so we
|
|
402
695
|
// cannot tell a user deletion apart from a never-created link — skip detection entirely.
|
|
@@ -405,42 +698,18 @@ async function deactivateDeletedAgentSkills(
|
|
|
405
698
|
}
|
|
406
699
|
|
|
407
700
|
const deletions = await deps.detectDeletedAgentSymlinks(
|
|
408
|
-
pullResponse.skills
|
|
701
|
+
pullResponse.skills.map(skill => {
|
|
702
|
+
const previous = isNativeSkill(skill) ? Object.entries(previousState.skills).find(([, row]) => row.cloudId === skill.id) : undefined;
|
|
703
|
+
return previous ? { ...skill, name: previous[0] } : skill;
|
|
704
|
+
}),
|
|
409
705
|
previousState,
|
|
410
706
|
skillsDir,
|
|
707
|
+
linkOptions,
|
|
411
708
|
);
|
|
412
709
|
if (deletions.length === 0) {
|
|
413
710
|
return 0;
|
|
414
711
|
}
|
|
415
712
|
|
|
416
|
-
// The agent-targets endpoint replaces the whole agent_targets column, so re-read the freshest
|
|
417
|
-
// values right before writing and merge onto them, only flipping the deleted agent. This avoids
|
|
418
|
-
// clobbering a concurrent portal/other-desktop change to a DIFFERENT agent with the stale
|
|
419
|
-
// snapshot from the top-of-sync pull. (A narrow read-modify-write window remains; the endpoint
|
|
420
|
-
// has no partial update.)
|
|
421
|
-
const latestSkillsById = new Map(pullResponse.skills.map((skill) => [skill.id, skill]));
|
|
422
|
-
let fresh: SyncPullResponse | null = null;
|
|
423
|
-
try {
|
|
424
|
-
fresh = await deps.pullSkills(serverUrl, jwt);
|
|
425
|
-
} catch (error) {
|
|
426
|
-
console.warn(
|
|
427
|
-
"[skill-sync] Could not re-pull latest agent targets before deactivation; " +
|
|
428
|
-
"using the top-of-sync snapshot.",
|
|
429
|
-
error,
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
if (fresh) {
|
|
433
|
-
// A transport failure may fall back to the already-authorized snapshot, but
|
|
434
|
-
// an explicit legacy denial must abort before any server or local mutation.
|
|
435
|
-
assertSkillsPullAuthorized(fresh);
|
|
436
|
-
for (const skill of fresh.skills) {
|
|
437
|
-
latestSkillsById.set(skill.id, skill);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Group deletions by skill so multiple deleted agents for the SAME skill are flipped in one
|
|
442
|
-
// PATCH. Otherwise each PATCH, rebuilt from the same snapshot, would overwrite the previous one
|
|
443
|
-
// (deleting both claude_code and cursor for one skill would otherwise leave only the last off).
|
|
444
713
|
const agentsBySkill = new Map<string, { skillName: string; agents: Set<DeletedAgentSymlink["agent"]> }>();
|
|
445
714
|
for (const deletion of deletions) {
|
|
446
715
|
const entry = agentsBySkill.get(deletion.skillId) ?? {
|
|
@@ -451,33 +720,47 @@ async function deactivateDeletedAgentSkills(
|
|
|
451
720
|
agentsBySkill.set(deletion.skillId, entry);
|
|
452
721
|
}
|
|
453
722
|
|
|
454
|
-
const
|
|
723
|
+
const fresh = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
724
|
+
assertSkillsPullAuthorized(fresh);
|
|
725
|
+
assertSyncEnvironment(fresh, pullResponse);
|
|
726
|
+
Object.assign(pullResponse, fresh);
|
|
727
|
+
let needsRefresh = false;
|
|
455
728
|
let deactivated = 0;
|
|
456
729
|
for (const [skillId, { skillName, agents }] of agentsBySkill) {
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
730
|
+
const skill = pullResponse.skills.find((item) => item.id === skillId);
|
|
731
|
+
const previous = previousState.skills[skillName];
|
|
732
|
+
if (!skill) continue;
|
|
733
|
+
const native = isNativeSkill(skill);
|
|
734
|
+
if (native) {
|
|
735
|
+
if (!skill.native_settings || previous?.nativeSettingsRevision !== skill.native_settings.revision) continue;
|
|
736
|
+
} else if (!skill.updated_at || previous?.cloudUpdatedAt !== skill.updated_at) continue;
|
|
737
|
+
const patch = Object.fromEntries([...agents].map((agent) => [agent, false]));
|
|
465
738
|
try {
|
|
466
|
-
await deps.updateAgentTargets(serverUrl, jwt, skillId,
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
739
|
+
const saved = await deps.updateAgentTargets(serverUrl, jwt, skillId, patch,
|
|
740
|
+
native ? undefined : skill.updated_at, native ? { reference: skill.native_reference!, settingsRevision: skill.native_settings!.revision } : undefined);
|
|
741
|
+
if (saved.success !== true || !saved.updated_at?.trim()
|
|
742
|
+
|| (!native && saved.updated_at === skill.updated_at)
|
|
743
|
+
|| !['notis', 'claude_code', 'cursor', 'codex'].every((agent) =>
|
|
744
|
+
typeof saved.agent_targets?.[agent as keyof AgentTargets] === 'boolean')
|
|
745
|
+
|| ![...agents].every((agent) => saved.agent_targets[agent] === false)) {
|
|
746
|
+
throw new Error('Assignment update did not return a verified saved revision');
|
|
471
747
|
}
|
|
748
|
+
skill.agent_targets = saved.agent_targets;
|
|
749
|
+
skill.updated_at = saved.updated_at;
|
|
750
|
+
if (native) needsRefresh = true;
|
|
472
751
|
deactivated += agents.size;
|
|
473
752
|
} catch (error) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
error,
|
|
478
|
-
);
|
|
753
|
+
needsRefresh = true;
|
|
754
|
+
failures.push({ name: skillName, error: 'Could not save the local agent removal; refreshed saved assignments' });
|
|
755
|
+
console.warn(`[skill-sync] Assignment changed or could not be saved for "${skillName}"; refreshing before reconciliation.`, error);
|
|
479
756
|
}
|
|
480
757
|
}
|
|
758
|
+
if (needsRefresh) {
|
|
759
|
+
const refreshed = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
760
|
+
assertSkillsPullAuthorized(refreshed);
|
|
761
|
+
assertSyncEnvironment(refreshed, pullResponse);
|
|
762
|
+
Object.assign(pullResponse, refreshed);
|
|
763
|
+
}
|
|
481
764
|
return deactivated;
|
|
482
765
|
}
|
|
483
766
|
|
|
@@ -487,9 +770,11 @@ export async function runSkillSync(
|
|
|
487
770
|
dependencies: Partial<RunSkillSyncDependencies> = {},
|
|
488
771
|
options: RunSkillSyncOptions = {},
|
|
489
772
|
): Promise<RunSkillSyncResult> {
|
|
773
|
+
const linkAgentFolders = options.linkAgentFolders !== false;
|
|
490
774
|
const deps = {
|
|
491
775
|
...DEFAULT_RUN_SKILL_SYNC_DEPS,
|
|
492
776
|
...dependencies,
|
|
777
|
+
...(linkAgentFolders ? {} : MIRROR_ONLY_DEPS),
|
|
493
778
|
};
|
|
494
779
|
const syncSettings: SyncSettings = await deps.fetchSyncSettings(
|
|
495
780
|
serverUrl,
|
|
@@ -506,11 +791,13 @@ export async function runSkillSync(
|
|
|
506
791
|
"Cannot sync skills without a server-verified account identity.",
|
|
507
792
|
);
|
|
508
793
|
}
|
|
509
|
-
const syncPaths = getSkillSyncPathsForUser(syncUserId);
|
|
510
|
-
const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
|
|
511
|
-
syncPaths.skillsDir,
|
|
512
|
-
);
|
|
794
|
+
const syncPaths = getSkillSyncPathsForUser(syncUserId, undefined, syncSettings.sync_namespace, options.syncHome);
|
|
513
795
|
if (options.honorSyncEnabled !== false && !syncSettings.sync_enabled) {
|
|
796
|
+
// Disabled sync intentionally performs no pull. Only the authenticated
|
|
797
|
+
// settings scope may remove foreign links; account mirrors stay untouched.
|
|
798
|
+
const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
|
|
799
|
+
syncPaths.skillsDir, isolatedLinkOptions(syncPaths),
|
|
800
|
+
);
|
|
514
801
|
return {
|
|
515
802
|
syncEnabled: false,
|
|
516
803
|
pushed: 0,
|
|
@@ -528,6 +815,13 @@ export async function runSkillSync(
|
|
|
528
815
|
|
|
529
816
|
let pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
530
817
|
assertSkillsPullAuthorized(pullResponse);
|
|
818
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
819
|
+
const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
|
|
820
|
+
syncPaths.skillsDir, isolatedLinkOptions(syncPaths),
|
|
821
|
+
);
|
|
822
|
+
if (linkAgentFolders && !syncPaths.isolatedNamespace) {
|
|
823
|
+
await forgetOtherAccountsAgentLinks(syncPaths);
|
|
824
|
+
}
|
|
531
825
|
|
|
532
826
|
const cloudCuratedSkillNames = new Set(
|
|
533
827
|
pullResponse.skills
|
|
@@ -535,56 +829,68 @@ export async function runSkillSync(
|
|
|
535
829
|
.map((skill) => skill.name),
|
|
536
830
|
);
|
|
537
831
|
const protectedSkillNames = new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
|
|
832
|
+
const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
|
|
833
|
+
const assignmentFailures: SkillSyncFailure[] = [];
|
|
834
|
+
// A mirror-only sync has no links by design, so none of them is missing.
|
|
835
|
+
const deactivated = linkAgentFolders && !syncPaths.isolatedNamespace
|
|
836
|
+
&& syncSettings.agent_targets_conditional_updates === true
|
|
837
|
+
? await deactivateDeletedAgentSkills(
|
|
838
|
+
serverUrl, jwt, pullResponse, scopedState, scopedState, syncPaths.skillsDir, deps, assignmentFailures, isolatedLinkOptions(syncPaths),
|
|
839
|
+
)
|
|
840
|
+
: 0;
|
|
841
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
538
842
|
const authUserId = decodeJwtSubject(jwt);
|
|
539
843
|
let previousAuthState: NotisSyncState | null = null;
|
|
540
|
-
if (authUserId && authUserId !== syncUserId) {
|
|
541
|
-
const previousAuthPaths = getSkillSyncPathsForUser(authUserId);
|
|
844
|
+
if (!syncPaths.isolatedNamespace && authUserId && authUserId !== syncUserId) {
|
|
845
|
+
const previousAuthPaths = getSkillSyncPathsForUser(authUserId, undefined, undefined, options.syncHome);
|
|
542
846
|
previousAuthState = await deps.readSyncState(previousAuthPaths);
|
|
543
847
|
await deps.gatherTopLevelLocalSkills(syncPaths, {
|
|
544
848
|
sourceRoots: [{ label: "previous-auth-scope", root: previousAuthPaths.skillsDir }],
|
|
545
849
|
protectedSkillNames,
|
|
546
850
|
});
|
|
547
851
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
852
|
+
if (linkAgentFolders && !syncPaths.isolatedNamespace) {
|
|
853
|
+
// Skills found loose in the agent folders belong to the account linking them.
|
|
854
|
+
await deps.gatherTopLevelLocalSkills(syncPaths, { protectedSkillNames });
|
|
855
|
+
}
|
|
551
856
|
const localSkills = (await deps.scanLocalSkills(syncPaths))
|
|
552
857
|
.filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
|
|
553
|
-
const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
|
|
554
858
|
const previousState = withoutBaseSkillState(applyLegacyFirstRunState(
|
|
555
859
|
localSkills,
|
|
556
860
|
scopedState,
|
|
557
|
-
isEmptySyncState(scopedState)
|
|
861
|
+
!syncPaths.isolatedNamespace && isEmptySyncState(scopedState)
|
|
558
862
|
? (!previousAuthState || isEmptySyncState(previousAuthState)
|
|
559
863
|
? await deps.readLegacySyncState(syncPaths)
|
|
560
864
|
: previousAuthState)
|
|
561
865
|
: null,
|
|
562
866
|
));
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const deactivated = await deactivateDeletedAgentSkills(
|
|
566
|
-
serverUrl,
|
|
567
|
-
jwt,
|
|
568
|
-
pullResponse,
|
|
569
|
-
previousState,
|
|
570
|
-
scopedState,
|
|
571
|
-
syncPaths.skillsDir,
|
|
572
|
-
deps,
|
|
573
|
-
);
|
|
574
|
-
await deps.syncSymlinks(
|
|
867
|
+
pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
|
|
868
|
+
const gatheredSymlinkResult = await deps.syncSymlinks(
|
|
575
869
|
buildLocalSymlinkCandidates(pullResponse, localSkills, previousState),
|
|
576
870
|
syncPaths.skillsDir,
|
|
871
|
+
isolatedLinkOptions(syncPaths),
|
|
577
872
|
);
|
|
578
873
|
const pushCandidates = getPushCandidates(
|
|
579
874
|
localSkills,
|
|
580
875
|
previousState,
|
|
581
876
|
cloudCuratedSkillNames,
|
|
582
877
|
new Set(pullResponse.skills.map((skill) => skill.name)),
|
|
878
|
+
pullResponse.skills,
|
|
583
879
|
);
|
|
584
880
|
|
|
585
881
|
const failedPushes: SkillSyncFailure[] = [];
|
|
882
|
+
const acknowledgedNative = new Map<string, string>();
|
|
883
|
+
let writeLocalSkills = localSkills;
|
|
586
884
|
if (pushCandidates.length > 0) {
|
|
587
885
|
const pushResult = await deps.pushChangedSkills(serverUrl, jwt, pushCandidates);
|
|
886
|
+
for (const result of pushResult.skills || []) {
|
|
887
|
+
const saved = result as { id?: string; native_reference?: unknown };
|
|
888
|
+
const candidate = pushCandidates.find(local => local.nativeReference && local.cloudId === saved.id);
|
|
889
|
+
if (candidate && saved.native_reference) {
|
|
890
|
+
checkedNativeReference(saved.native_reference, candidate.cloudId);
|
|
891
|
+
acknowledgedNative.set(candidate.cloudId!, candidate.folderHash);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
588
894
|
if (Array.isArray(pushResult?.failed) && pushResult.failed.length > 0) {
|
|
589
895
|
failedPushes.push(...pushResult.failed);
|
|
590
896
|
console.warn(
|
|
@@ -594,6 +900,9 @@ export async function runSkillSync(
|
|
|
594
900
|
}
|
|
595
901
|
pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
596
902
|
assertSkillsPullAuthorized(pullResponse);
|
|
903
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
904
|
+
writeLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter(skill => !BASE_SKILL_NAMES.has(skill.name));
|
|
905
|
+
pullResponse = bindNativeFolders(pullResponse, writeLocalSkills, previousState);
|
|
597
906
|
}
|
|
598
907
|
|
|
599
908
|
// Delete phase: remove skills that were previously synced but are no longer in the cloud
|
|
@@ -601,18 +910,31 @@ export async function runSkillSync(
|
|
|
601
910
|
let deleted = 0;
|
|
602
911
|
for (const skillName of Object.keys(previousState.skills)) {
|
|
603
912
|
if (!cloudSkillNames.has(skillName)) {
|
|
913
|
+
const previous = previousState.skills[skillName];
|
|
914
|
+
if (previous.nativeReference || previous.nativeUnavailable
|
|
915
|
+
|| pullResponse.skills.some(skill => isNativeSkill(skill) && skill.id === previous.cloudId)) {
|
|
916
|
+
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
604
919
|
await deps.deleteLocalSkill(skillName, syncPaths);
|
|
605
|
-
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir);
|
|
920
|
+
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
606
921
|
deleted += 1;
|
|
607
922
|
}
|
|
608
923
|
}
|
|
609
924
|
|
|
925
|
+
const failedDownloads: SkillSyncFailure[] = [];
|
|
926
|
+
const writtenSkillNames = new Set<string>();
|
|
927
|
+
const appliedFilesByName: Record<string, string[]> = {};
|
|
610
928
|
const downloaded = await writePulledSkillsToScopedMirror(
|
|
611
929
|
pullResponse,
|
|
612
|
-
|
|
930
|
+
writeLocalSkills,
|
|
613
931
|
previousState,
|
|
614
932
|
syncPaths,
|
|
615
933
|
deps,
|
|
934
|
+
failedDownloads,
|
|
935
|
+
writtenSkillNames,
|
|
936
|
+
appliedFilesByName,
|
|
937
|
+
acknowledgedNative,
|
|
616
938
|
);
|
|
617
939
|
|
|
618
940
|
const finalLocalSkills = (await deps.scanLocalSkills(syncPaths))
|
|
@@ -620,11 +942,23 @@ export async function runSkillSync(
|
|
|
620
942
|
const symlinkResult = await deps.syncSymlinks(
|
|
621
943
|
buildLocalSymlinkCandidates(pullResponse, finalLocalSkills, previousState),
|
|
622
944
|
syncPaths.skillsDir,
|
|
945
|
+
isolatedLinkOptions(syncPaths),
|
|
623
946
|
);
|
|
947
|
+
const verifiedLinks = { ...(symlinkResult.verifiedAgentLinks ?? {}) };
|
|
948
|
+
for (const failure of failedDownloads) delete verifiedLinks[failure.name];
|
|
624
949
|
const lastSyncedAt = pullResponse.last_synced_at || new Date().toISOString();
|
|
625
950
|
|
|
626
951
|
await deps.writeSyncState(
|
|
627
|
-
buildSyncState(
|
|
952
|
+
buildSyncState(
|
|
953
|
+
pullResponse,
|
|
954
|
+
finalLocalSkills,
|
|
955
|
+
lastSyncedAt,
|
|
956
|
+
verifiedLinks,
|
|
957
|
+
new Set(failedDownloads.map(item => item.name)),
|
|
958
|
+
collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
|
|
959
|
+
collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
|
|
960
|
+
previousState,
|
|
961
|
+
),
|
|
628
962
|
syncPaths,
|
|
629
963
|
);
|
|
630
964
|
|
|
@@ -635,9 +969,12 @@ export async function runSkillSync(
|
|
|
635
969
|
downloaded,
|
|
636
970
|
deleted,
|
|
637
971
|
deactivated,
|
|
638
|
-
linked: symlinkResult.linked,
|
|
639
|
-
removed: foreignLinksRemoved + symlinkResult.removed,
|
|
972
|
+
linked: gatheredSymlinkResult.linked + symlinkResult.linked,
|
|
973
|
+
removed: foreignLinksRemoved + gatheredSymlinkResult.removed + symlinkResult.removed,
|
|
640
974
|
skipped: symlinkResult.skipped,
|
|
975
|
+
failedLinks: [...assignmentFailures, ...failedDownloads, ...(symlinkResult.failures ?? []).filter(
|
|
976
|
+
(failure) => !failedDownloads.some((download) => download.name === failure.name),
|
|
977
|
+
)],
|
|
641
978
|
lastSyncedAt,
|
|
642
979
|
failedPushes,
|
|
643
980
|
};
|