@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
package/dist/skill-sync/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// src/runtime/skill-sync/index.ts
|
|
2
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3
|
+
import { promises as fs3 } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
|
|
1
6
|
// src/runtime/skill-sync/types.ts
|
|
2
7
|
var DEFAULT_AGENT_TARGETS = {
|
|
3
8
|
notis: true,
|
|
@@ -14,8 +19,133 @@ function normalizeAgentTargets(targets) {
|
|
|
14
19
|
};
|
|
15
20
|
}
|
|
16
21
|
|
|
22
|
+
// src/runtime/skill-sync/native-identity.ts
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
var NATIVE_IDENTITY_FILE = ".notis-native-skill.json";
|
|
25
|
+
var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
26
|
+
function checkedNativeReference(value, cloudId) {
|
|
27
|
+
const reference = value;
|
|
28
|
+
if (!reference || typeof reference !== "object" || Array.isArray(reference) || Object.keys(reference).sort().join(",") !== "access_revision,kind,skill_id,version" || reference.kind !== "native_standalone_skill" || !UUID.test(reference.skill_id) || cloudId !== void 0 && reference.skill_id !== cloudId || !Number.isSafeInteger(reference.access_revision) || reference.access_revision < 0 || !reference.version || Object.keys(reference.version).sort().join(",") !== "digest,revision" || !Number.isSafeInteger(reference.version.revision) || reference.version.revision < 1 || !/^[a-f0-9]{64}$/.test(reference.version.digest)) {
|
|
29
|
+
throw new Error("Native Skill identity is unavailable. Refresh before syncing.");
|
|
30
|
+
}
|
|
31
|
+
return structuredClone(reference);
|
|
32
|
+
}
|
|
33
|
+
function nativeDirectory(reference) {
|
|
34
|
+
return "native-" + checkedNativeReference(reference).skill_id;
|
|
35
|
+
}
|
|
36
|
+
function sameNativeReference(left, right) {
|
|
37
|
+
return Boolean(left && right && left.skill_id === right.skill_id && left.access_revision === right.access_revision && left.version.revision === right.version.revision && left.version.digest === right.version.digest);
|
|
38
|
+
}
|
|
39
|
+
function isNativeSkill(skill) {
|
|
40
|
+
return skill.source === "native" || skill.native_reference !== void 0;
|
|
41
|
+
}
|
|
42
|
+
function assertNativePull(response) {
|
|
43
|
+
const ids = /* @__PURE__ */ new Set();
|
|
44
|
+
for (const skill of response.skills) {
|
|
45
|
+
if (!isNativeSkill(skill)) continue;
|
|
46
|
+
const reference = checkedNativeReference(skill.native_reference, skill.id);
|
|
47
|
+
if (response.skill_sync_protocol !== 2 || skill.name !== nativeDirectory(reference) || ids.has(skill.id) || !/^[a-f0-9]{64}$/.test(response.sync_scope || "") || skill.native_scope !== response.sync_scope || !Array.isArray(skill.bundle_files) || skill.bundle_hydration_failed || typeof skill.skill_md !== "string") {
|
|
48
|
+
throw new Error("Native Skill inventory is incomplete. Existing local files were retained.");
|
|
49
|
+
}
|
|
50
|
+
const paths = /* @__PURE__ */ new Set();
|
|
51
|
+
let entrypoint = false;
|
|
52
|
+
for (const file of skill.bundle_files) {
|
|
53
|
+
if (typeof file.path !== "string" || typeof file.content_b64 !== "string" || file.path.startsWith("/") || file.path.includes("\\") || file.path.includes("\0") || file.path.split("/").some((part) => !part || part === "." || part === "..") || paths.has(file.path.toLowerCase()) || file.path.toLowerCase() === NATIVE_IDENTITY_FILE) {
|
|
54
|
+
throw new Error("Native Skill bundle has a conflicting managed path.");
|
|
55
|
+
}
|
|
56
|
+
paths.add(file.path.toLowerCase());
|
|
57
|
+
if (file.path === "SKILL.md") {
|
|
58
|
+
entrypoint = Buffer.from(file.content_b64, "base64").equals(Buffer.from(skill.skill_md, "utf8"));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!entrypoint) throw new Error("Native Skill instructions and files do not match.");
|
|
62
|
+
ids.add(skill.id);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function nativeFileDigest(bytes) {
|
|
66
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
67
|
+
}
|
|
68
|
+
function nativeFrontmatterName(markdown) {
|
|
69
|
+
const header = markdown.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
70
|
+
return header?.[1].match(/^name\s*:\s*(.+)$/m)?.[1].trim().replace(/^['"]|['"]$/g, "") || "";
|
|
71
|
+
}
|
|
72
|
+
function checkedNativeIdentity(value) {
|
|
73
|
+
const identity = value;
|
|
74
|
+
if (!identity || identity.schema !== 1 || typeof identity.cloudId !== "string" || typeof identity.displayName !== "string" || typeof identity.frontmatterName !== "string" || !/^[a-f0-9]{64}$/.test(identity.scope) || !/^[a-f0-9]{64}$/.test(identity.folderHash) || !identity.files || !Object.keys(identity.files).includes("SKILL.md") || Object.entries(identity.files).some(([name, hash]) => name.startsWith("/") || name.includes("\\") || name.split("/").some((part) => !part || part === "." || part === "..") || !/^[a-f0-9]{64}$/.test(hash))) {
|
|
75
|
+
throw new Error("Native Skill provenance is incomplete. Existing files were retained.");
|
|
76
|
+
}
|
|
77
|
+
return { ...identity, reference: checkedNativeReference(identity.reference, identity.cloudId) };
|
|
78
|
+
}
|
|
79
|
+
function nativeState(skill) {
|
|
80
|
+
if (!isNativeSkill(skill)) return {};
|
|
81
|
+
return {
|
|
82
|
+
nativeReference: checkedNativeReference(skill.native_reference, skill.id),
|
|
83
|
+
...skill.native_settings ? { nativeSettingsRevision: skill.native_settings.revision } : {},
|
|
84
|
+
nativeDisplayName: skill.display_name || skill.name,
|
|
85
|
+
nativeScope: skill.native_scope,
|
|
86
|
+
nativeUnavailable: false
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/runtime/skill-sync/native-plan.ts
|
|
91
|
+
function bindNativeFolders(response, locals, state) {
|
|
92
|
+
const nativeIds = new Set(response.skills.filter(isNativeSkill).map((skill) => skill.id));
|
|
93
|
+
for (const entry of Object.values(state.skills)) if (entry.nativeReference || entry.nativeUnavailable) nativeIds.add(entry.cloudId);
|
|
94
|
+
const byId = /* @__PURE__ */ new Map();
|
|
95
|
+
for (const local of locals) {
|
|
96
|
+
const id = local.cloudId || state.skills[local.name]?.cloudId;
|
|
97
|
+
if (!id || !nativeIds.has(id) && !local.nativeReference) continue;
|
|
98
|
+
if (local.nativeReference) checkedNativeReference(local.nativeReference, id);
|
|
99
|
+
if (byId.has(id)) throw new Error("Two local folders claim the same native Skill. Keep both copies and resolve the conflict.");
|
|
100
|
+
byId.set(id, local);
|
|
101
|
+
}
|
|
102
|
+
const skills = response.skills.map((skill) => isNativeSkill(skill) && byId.has(skill.id) ? { ...skill, name: byId.get(skill.id).name } : skill);
|
|
103
|
+
for (const skill of skills.filter(isNativeSkill)) {
|
|
104
|
+
if (skills.some((other) => other.id !== skill.id && other.name === skill.name)) {
|
|
105
|
+
throw new Error("A native Skill and another Skill share a local folder. Existing files were retained.");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { ...response, skills };
|
|
109
|
+
}
|
|
110
|
+
function previousNativeState(state, skill) {
|
|
111
|
+
const named = state.skills[skill.name];
|
|
112
|
+
return named?.cloudId === skill.id ? named : Object.values(state.skills).find((entry) => entry.cloudId === skill.id);
|
|
113
|
+
}
|
|
114
|
+
function nativeLocalReady(skill, local) {
|
|
115
|
+
return Boolean(local?.nativeFilesCurrent && local.folderHash === local.nativeBaseFolderHash && local.cloudId === skill.id && local.nativeScope === skill.native_scope && sameNativeReference(local.nativeReference, skill.native_reference));
|
|
116
|
+
}
|
|
117
|
+
function pendingLegacyUpgrade(skill, local, state) {
|
|
118
|
+
if (!isNativeSkill(skill) || !local || local.nativeReference || local.cloudId) return false;
|
|
119
|
+
const previous = previousNativeState(state, skill);
|
|
120
|
+
return Boolean(previous && !previous.nativeReference);
|
|
121
|
+
}
|
|
122
|
+
function nativeLinkRows(response, locals, state) {
|
|
123
|
+
return response.skills.map((skill) => {
|
|
124
|
+
const local = locals.find((row) => row.name === skill.name);
|
|
125
|
+
return isNativeSkill(skill) && !nativeLocalReady(skill, local) && !(state && pendingLegacyUpgrade(skill, local, state)) ? { ...skill, status: "disabled" } : skill;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function retainMissingNativeState(previous, next) {
|
|
129
|
+
const present = new Set(Object.values(next.skills).map((row) => row.cloudId));
|
|
130
|
+
const retained = Object.fromEntries(Object.entries(previous.skills).filter(([, row]) => (row.nativeReference || row.nativeUnavailable) && !present.has(row.cloudId)).map(([name, row]) => [name, { ...row, nativeUnavailable: true, verifiedAgentLinks: {} }]));
|
|
131
|
+
const history = structuredClone(previous.nativeHistory || {});
|
|
132
|
+
for (const [name, row] of [...Object.entries(previous.skills), ...Object.entries(next.skills)]) {
|
|
133
|
+
if (!row.nativeReference && !row.nativeUnavailable) continue;
|
|
134
|
+
const names = history[row.cloudId]?.names || [];
|
|
135
|
+
history[row.cloudId] = { names: [.../* @__PURE__ */ new Set([...names, name])] };
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
...next,
|
|
139
|
+
skills: { ...retained, ...next.skills },
|
|
140
|
+
...Object.keys(history).length ? { nativeHistory: history } : {}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function hasNativeFolderHistory(state, name) {
|
|
144
|
+
return Object.values(state.nativeHistory || {}).some((row) => row.names.includes(name));
|
|
145
|
+
}
|
|
146
|
+
|
|
17
147
|
// src/runtime/skill-sync/local-scanner.ts
|
|
18
|
-
import { createHash } from "crypto";
|
|
148
|
+
import { createHash as createHash2 } from "crypto";
|
|
19
149
|
import { execFile } from "child_process";
|
|
20
150
|
import { promises as fs } from "fs";
|
|
21
151
|
import os from "os";
|
|
@@ -23,6 +153,8 @@ import path from "path";
|
|
|
23
153
|
import { promisify } from "util";
|
|
24
154
|
var HOME_DIR = os.homedir();
|
|
25
155
|
var execFileAsync = promisify(execFile);
|
|
156
|
+
var NativeSkillIdentityError = class extends Error {
|
|
157
|
+
};
|
|
26
158
|
var AGENTS_DIR = path.join(HOME_DIR, ".agents");
|
|
27
159
|
var LEGACY_AGENTS_SKILLS_DIR = path.join(AGENTS_DIR, "skills");
|
|
28
160
|
var NOTIS_SKILL_SYNC_ROOT = path.join(HOME_DIR, ".notis", "skills");
|
|
@@ -82,15 +214,18 @@ function sanitizePathSegment(value) {
|
|
|
82
214
|
if (sanitized === raw) {
|
|
83
215
|
return sanitized;
|
|
84
216
|
}
|
|
85
|
-
return `${sanitized}-${
|
|
217
|
+
return `${sanitized}-${createHash2("sha256").update(raw).digest("hex").slice(0, 12)}`;
|
|
86
218
|
}
|
|
87
|
-
function getSkillSyncPathsForUser(authUserId, agentsDir = AGENTS_DIR) {
|
|
219
|
+
function getSkillSyncPathsForUser(authUserId, agentsDir = AGENTS_DIR, namespace, syncHome) {
|
|
88
220
|
const safeUserId = sanitizePathSegment(authUserId);
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
221
|
+
const homeDir = syncHome ? path.resolve(syncHome) : void 0;
|
|
222
|
+
const resolvedAgentsDir = homeDir ? path.join(homeDir, ".agents") : path.resolve(agentsDir);
|
|
223
|
+
const syncRoot = homeDir ? path.join(homeDir, ".notis", "skills") : getDefaultSyncRootForAgentsDir(resolvedAgentsDir);
|
|
224
|
+
const userRoot = namespace ? path.join(syncRoot, "environments", sanitizePathSegment(namespace), "users", safeUserId) : path.join(syncRoot, "users", safeUserId);
|
|
92
225
|
const legacyUserRoot = path.join(resolvedAgentsDir, "notis", "users", safeUserId);
|
|
93
226
|
return {
|
|
227
|
+
...homeDir ? { homeDir } : {},
|
|
228
|
+
...namespace ? { isolatedNamespace: sanitizePathSegment(namespace) } : {},
|
|
94
229
|
agentsDir: resolvedAgentsDir,
|
|
95
230
|
syncRoot,
|
|
96
231
|
legacySkillsDir: path.join(resolvedAgentsDir, "skills"),
|
|
@@ -139,16 +274,23 @@ async function readJsonFile(filePath) {
|
|
|
139
274
|
return null;
|
|
140
275
|
}
|
|
141
276
|
}
|
|
142
|
-
|
|
277
|
+
var IGNORED_SKILL_DIRECTORIES = /* @__PURE__ */ new Set(["__pycache__", ".pytest_cache"]);
|
|
278
|
+
function isIgnoredSkillEntry(entry) {
|
|
279
|
+
if (entry.name === ".DS_Store") return true;
|
|
280
|
+
if (entry.isDirectory()) return IGNORED_SKILL_DIRECTORIES.has(entry.name);
|
|
281
|
+
return /\.py[co]$/i.test(entry.name);
|
|
282
|
+
}
|
|
283
|
+
async function listFilesRecursive(dirPath, rootPath = dirPath, strictLinks = false) {
|
|
143
284
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
144
285
|
const nested = await Promise.all(
|
|
145
286
|
entries.map(async (entry) => {
|
|
146
|
-
if (entry.name ===
|
|
287
|
+
if (isIgnoredSkillEntry(entry) || dirPath === rootPath && entry.name === NATIVE_IDENTITY_FILE) {
|
|
147
288
|
return [];
|
|
148
289
|
}
|
|
149
290
|
const fullPath = path.join(dirPath, entry.name);
|
|
291
|
+
if (strictLinks && entry.isSymbolicLink()) throw new Error("Native Skill bundles cannot contain symbolic links.");
|
|
150
292
|
if (entry.isDirectory()) {
|
|
151
|
-
return listFilesRecursive(fullPath);
|
|
293
|
+
return listFilesRecursive(fullPath, rootPath, strictLinks);
|
|
152
294
|
}
|
|
153
295
|
if (entry.isFile()) {
|
|
154
296
|
return [fullPath];
|
|
@@ -159,7 +301,7 @@ async function listFilesRecursive(dirPath) {
|
|
|
159
301
|
return nested.flat().sort();
|
|
160
302
|
}
|
|
161
303
|
async function computeFolderHash(dirPath) {
|
|
162
|
-
const hash =
|
|
304
|
+
const hash = createHash2("sha256");
|
|
163
305
|
const filePaths = await listFilesRecursive(dirPath);
|
|
164
306
|
for (const filePath of filePaths) {
|
|
165
307
|
const relativePath = path.relative(dirPath, filePath);
|
|
@@ -237,17 +379,17 @@ function defaultTopLevelSkillSources(paths) {
|
|
|
237
379
|
{ label: "agents", root: paths.legacySkillsDir, priority: 2 },
|
|
238
380
|
{
|
|
239
381
|
label: "codex",
|
|
240
|
-
root: path.join(HOME_DIR, ".codex", "skills"),
|
|
382
|
+
root: path.join(paths.homeDir || HOME_DIR, ".codex", "skills"),
|
|
241
383
|
priority: 3
|
|
242
384
|
},
|
|
243
385
|
{
|
|
244
386
|
label: "cursor",
|
|
245
|
-
root: path.join(HOME_DIR, ".cursor", "skills"),
|
|
387
|
+
root: path.join(paths.homeDir || HOME_DIR, ".cursor", "skills"),
|
|
246
388
|
priority: 4
|
|
247
389
|
},
|
|
248
390
|
{
|
|
249
391
|
label: "claude",
|
|
250
|
-
root: path.join(HOME_DIR, ".claude", "skills"),
|
|
392
|
+
root: path.join(paths.homeDir || HOME_DIR, ".claude", "skills"),
|
|
251
393
|
priority: 5
|
|
252
394
|
}
|
|
253
395
|
);
|
|
@@ -467,6 +609,7 @@ async function ensureCanonicalSkillsDir(paths = DEFAULT_SYNC_PATHS) {
|
|
|
467
609
|
async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
|
|
468
610
|
await ensureCanonicalSkillsDir(paths);
|
|
469
611
|
const sourceUrls = await readSkillLock(paths);
|
|
612
|
+
const previousState = await readSyncState(paths);
|
|
470
613
|
const entries = await fs.readdir(paths.skillsDir, { withFileTypes: true });
|
|
471
614
|
const skills = await Promise.all(
|
|
472
615
|
entries.map(async (entry) => {
|
|
@@ -475,6 +618,15 @@ async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
|
|
|
475
618
|
}
|
|
476
619
|
const skillDir = path.join(paths.skillsDir, entry.name);
|
|
477
620
|
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
621
|
+
let marker = null;
|
|
622
|
+
try {
|
|
623
|
+
marker = await fs.readFile(path.join(skillDir, NATIVE_IDENTITY_FILE), "utf8");
|
|
624
|
+
} catch (error) {
|
|
625
|
+
if (error.code !== "ENOENT") {
|
|
626
|
+
throw new NativeSkillIdentityError("Native Skill provenance could not be read; existing files were retained.");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const knownNative = marker !== null || Boolean(previousState.skills[entry.name]?.nativeReference) || Object.values(previousState.nativeHistory || {}).some((row) => row.names.includes(entry.name));
|
|
478
630
|
try {
|
|
479
631
|
const skillMd = await fs.readFile(skillMdPath, "utf8");
|
|
480
632
|
const { description } = parseFrontMatter(skillMd);
|
|
@@ -486,11 +638,38 @@ async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
|
|
|
486
638
|
folderHash,
|
|
487
639
|
directoryPath: skillDir
|
|
488
640
|
};
|
|
641
|
+
if (marker !== null) {
|
|
642
|
+
let identity;
|
|
643
|
+
try {
|
|
644
|
+
identity = checkedNativeIdentity(JSON.parse(marker));
|
|
645
|
+
} catch {
|
|
646
|
+
throw new NativeSkillIdentityError("Native Skill provenance is unreadable; do not upload it as a new Skill.");
|
|
647
|
+
}
|
|
648
|
+
let current = true;
|
|
649
|
+
for (const [name, expected] of Object.entries(identity.files)) {
|
|
650
|
+
try {
|
|
651
|
+
if (nativeFileDigest(await fs.readFile(resolveSkillBundleFilePath(skillDir, name))) !== expected) current = false;
|
|
652
|
+
} catch {
|
|
653
|
+
current = false;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const editedName = nativeFrontmatterName(skillMd);
|
|
657
|
+
Object.assign(skill, {
|
|
658
|
+
cloudId: identity.cloudId,
|
|
659
|
+
nativeReference: identity.reference,
|
|
660
|
+
nativeScope: identity.scope,
|
|
661
|
+
nativeBaseFolderHash: identity.folderHash,
|
|
662
|
+
nativeFilesCurrent: current,
|
|
663
|
+
displayName: editedName && editedName !== identity.frontmatterName ? editedName : identity.displayName
|
|
664
|
+
});
|
|
665
|
+
}
|
|
489
666
|
if (sourceUrls[entry.name]) {
|
|
490
667
|
skill.sourceUrl = sourceUrls[entry.name];
|
|
491
668
|
}
|
|
492
669
|
return skill;
|
|
493
|
-
} catch {
|
|
670
|
+
} catch (error) {
|
|
671
|
+
if (error instanceof NativeSkillIdentityError) throw error;
|
|
672
|
+
if (knownNative) throw new NativeSkillIdentityError("Native Skill files could not be read; existing files were retained.");
|
|
494
673
|
return null;
|
|
495
674
|
}
|
|
496
675
|
})
|
|
@@ -501,6 +680,9 @@ function normalizeSyncState(state) {
|
|
|
501
680
|
if (!state || state.version !== 1 || typeof state.skills !== "object") {
|
|
502
681
|
return null;
|
|
503
682
|
}
|
|
683
|
+
if (state.nativeHistory && (typeof state.nativeHistory !== "object" || Array.isArray(state.nativeHistory) || Object.values(state.nativeHistory).some((row) => !row || !Array.isArray(row.names) || row.names.some((name) => typeof name !== "string")))) {
|
|
684
|
+
throw new Error("Native Skill sync provenance is unreadable; existing files were retained.");
|
|
685
|
+
}
|
|
504
686
|
const normalizeStoredAgentTargets = (targets) => ({
|
|
505
687
|
notis: Boolean(targets?.notis ?? true),
|
|
506
688
|
claude_code: Boolean(targets?.claude_code ?? true),
|
|
@@ -509,6 +691,7 @@ function normalizeSyncState(state) {
|
|
|
509
691
|
});
|
|
510
692
|
return {
|
|
511
693
|
version: 1,
|
|
694
|
+
...state.nativeHistory ? { nativeHistory: state.nativeHistory } : {},
|
|
512
695
|
lastSyncedAt: typeof state.lastSyncedAt === "string" ? state.lastSyncedAt : null,
|
|
513
696
|
skills: Object.fromEntries(
|
|
514
697
|
Object.entries(state.skills || {}).map(([skillName, skillState]) => [
|
|
@@ -583,20 +766,49 @@ async function createZipFromDirectory(directoryPath) {
|
|
|
583
766
|
os.tmpdir(),
|
|
584
767
|
`notis-skill-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`
|
|
585
768
|
);
|
|
586
|
-
const parentDir = path.dirname(directoryPath);
|
|
587
769
|
const directoryName = path.basename(directoryPath);
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
770
|
+
const stagingRoot = await fs.mkdtemp(path.join(os.tmpdir(), "notis-skill-bundle-"));
|
|
771
|
+
try {
|
|
772
|
+
const stagedDir = path.join(stagingRoot, directoryName);
|
|
773
|
+
for (const filePath of await listFilesRecursive(directoryPath)) {
|
|
774
|
+
const target = path.join(stagedDir, path.relative(directoryPath, filePath));
|
|
775
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
776
|
+
await fs.copyFile(filePath, target);
|
|
777
|
+
}
|
|
778
|
+
await fs.mkdir(stagedDir, { recursive: true });
|
|
779
|
+
if (process.platform === "win32") {
|
|
780
|
+
const systemTar = path.join(
|
|
781
|
+
process.env.SystemRoot || "C:\\Windows",
|
|
782
|
+
"System32",
|
|
783
|
+
"tar.exe"
|
|
784
|
+
);
|
|
785
|
+
if (await pathExists(systemTar)) {
|
|
786
|
+
await execFileAsync(systemTar, [
|
|
787
|
+
"-c",
|
|
788
|
+
"--format",
|
|
789
|
+
"zip",
|
|
790
|
+
"-f",
|
|
791
|
+
zipPath,
|
|
792
|
+
"-C",
|
|
793
|
+
stagingRoot,
|
|
794
|
+
directoryName
|
|
795
|
+
]);
|
|
796
|
+
return zipPath;
|
|
797
|
+
}
|
|
798
|
+
await execFileAsync("powershell.exe", [
|
|
799
|
+
"-NoProfile",
|
|
800
|
+
"-Command",
|
|
801
|
+
`Compress-Archive -Path '${stagedDir.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`
|
|
802
|
+
]);
|
|
803
|
+
return zipPath;
|
|
804
|
+
}
|
|
805
|
+
await execFileAsync("zip", ["-qry", zipPath, directoryName], {
|
|
806
|
+
cwd: stagingRoot
|
|
807
|
+
});
|
|
594
808
|
return zipPath;
|
|
809
|
+
} finally {
|
|
810
|
+
await fs.rm(stagingRoot, { recursive: true, force: true });
|
|
595
811
|
}
|
|
596
|
-
await execFileAsync("zip", ["-qry", zipPath, directoryName], {
|
|
597
|
-
cwd: parentDir
|
|
598
|
-
});
|
|
599
|
-
return zipPath;
|
|
600
812
|
}
|
|
601
813
|
async function extractZipToDirectory(zipPath, destinationDir) {
|
|
602
814
|
const extractRoot = await fs.mkdtemp(
|
|
@@ -622,10 +834,39 @@ async function extractZipToDirectory(zipPath, destinationDir) {
|
|
|
622
834
|
await fs.rm(destinationDir, { recursive: true, force: true });
|
|
623
835
|
await fs.mkdir(path.dirname(destinationDir), { recursive: true });
|
|
624
836
|
await fs.cp(sourceDir, destinationDir, { recursive: true });
|
|
837
|
+
return await listRelativeFiles(destinationDir);
|
|
625
838
|
} finally {
|
|
626
839
|
await fs.rm(extractRoot, { recursive: true, force: true });
|
|
627
840
|
}
|
|
628
841
|
}
|
|
842
|
+
async function listRelativeFiles(dirPath) {
|
|
843
|
+
try {
|
|
844
|
+
const files = await listFilesRecursive(dirPath);
|
|
845
|
+
return files.map((filePath) => toPosixRelativePath(dirPath, filePath)).sort();
|
|
846
|
+
} catch (error) {
|
|
847
|
+
if (error?.code === "ENOENT") return [];
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function toPosixRelativePath(rootDir, filePath) {
|
|
852
|
+
return path.relative(rootDir, filePath).split(path.sep).join("/");
|
|
853
|
+
}
|
|
854
|
+
async function preserveUnmanagedFiles(existingDir, stagingDir, previouslyApplied, strictConflicts = false) {
|
|
855
|
+
const managed = previouslyApplied ? new Set(previouslyApplied) : null;
|
|
856
|
+
for (const existingFile of await listFilesRecursive(existingDir)) {
|
|
857
|
+
const relativePath = toPosixRelativePath(existingDir, existingFile);
|
|
858
|
+
if (managed?.has(relativePath)) continue;
|
|
859
|
+
const stagedPath = path.join(stagingDir, ...relativePath.split("/"));
|
|
860
|
+
if (await pathExists(stagedPath)) {
|
|
861
|
+
if (strictConflicts && !(await fs.readFile(existingFile)).equals(await fs.readFile(stagedPath))) {
|
|
862
|
+
throw new Error("An incoming native Skill file conflicts with a local file. Local files were retained.");
|
|
863
|
+
}
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
await fs.mkdir(path.dirname(stagedPath), { recursive: true });
|
|
867
|
+
await fs.copyFile(existingFile, stagedPath);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
629
870
|
async function pathExists(targetPath) {
|
|
630
871
|
try {
|
|
631
872
|
await fs.access(targetPath);
|
|
@@ -634,7 +875,7 @@ async function pathExists(targetPath) {
|
|
|
634
875
|
return false;
|
|
635
876
|
}
|
|
636
877
|
}
|
|
637
|
-
async function replaceSkillDirectoryAtomically(skillDir, populateDir) {
|
|
878
|
+
async function replaceSkillDirectoryAtomically(skillDir, populateDir, preserve) {
|
|
638
879
|
const parentDir = path.dirname(skillDir);
|
|
639
880
|
const skillName = path.basename(skillDir);
|
|
640
881
|
await fs.mkdir(parentDir, { recursive: true });
|
|
@@ -649,10 +890,46 @@ async function replaceSkillDirectoryAtomically(skillDir, populateDir) {
|
|
|
649
890
|
let promotedStaging = false;
|
|
650
891
|
let cleanupError = null;
|
|
651
892
|
try {
|
|
893
|
+
if (preserve?.native && preserve.expectedFolderHash === void 0) {
|
|
894
|
+
let existing = false;
|
|
895
|
+
try {
|
|
896
|
+
await fs.lstat(skillDir);
|
|
897
|
+
existing = true;
|
|
898
|
+
} catch (error) {
|
|
899
|
+
if (error.code !== "ENOENT") throw error;
|
|
900
|
+
}
|
|
901
|
+
if (existing) throw new Error("Existing native Skill files need a readable baseline before replacement.");
|
|
902
|
+
}
|
|
903
|
+
if (preserve?.expectedFolderHash !== void 0 && (!await pathExists(skillDir) || await computeFolderHash(skillDir) !== preserve.expectedFolderHash)) {
|
|
904
|
+
throw new Error("Local Skill files changed before sync; the newer local edits were retained.");
|
|
905
|
+
}
|
|
652
906
|
await populateDir(stagingDir);
|
|
907
|
+
if (preserve && await pathExists(skillDir)) {
|
|
908
|
+
await preserveUnmanagedFiles(
|
|
909
|
+
skillDir,
|
|
910
|
+
stagingDir,
|
|
911
|
+
preserve.previouslyApplied,
|
|
912
|
+
preserve.native && !preserve.legacyBaseline
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
if (preserve?.native) {
|
|
916
|
+
const markerPath = path.join(stagingDir, NATIVE_IDENTITY_FILE);
|
|
917
|
+
const marker = checkedNativeIdentity(JSON.parse(await fs.readFile(markerPath, "utf8")));
|
|
918
|
+
marker.folderHash = await computeFolderHash(stagingDir);
|
|
919
|
+
await fs.writeFile(markerPath, JSON.stringify(marker), { mode: 384 });
|
|
920
|
+
}
|
|
921
|
+
if (preserve?.expectedFolderHash !== void 0 && (!await pathExists(skillDir) || await computeFolderHash(skillDir) !== preserve.expectedFolderHash)) {
|
|
922
|
+
throw new Error("Local Skill files changed during sync; the newer local edits were retained.");
|
|
923
|
+
}
|
|
653
924
|
if (await pathExists(skillDir)) {
|
|
654
925
|
await fs.rename(skillDir, backupDir);
|
|
655
926
|
movedExisting = true;
|
|
927
|
+
if (preserve?.native && preserve.expectedFolderHash === void 0) {
|
|
928
|
+
throw new Error("Native Skill files appeared during sync; existing files were retained.");
|
|
929
|
+
}
|
|
930
|
+
if (preserve?.expectedFolderHash !== void 0 && await computeFolderHash(backupDir) !== preserve.expectedFolderHash) {
|
|
931
|
+
throw new Error("Local Skill files changed before publication; the newer local edits were retained.");
|
|
932
|
+
}
|
|
656
933
|
}
|
|
657
934
|
await fs.rename(stagingDir, skillDir);
|
|
658
935
|
promotedStaging = true;
|
|
@@ -697,11 +974,43 @@ async function createSkillBundleBase64(skill) {
|
|
|
697
974
|
await fs.rm(zipPath, { force: true });
|
|
698
975
|
}
|
|
699
976
|
}
|
|
700
|
-
async function
|
|
977
|
+
async function captureNativeSkillFiles(skill) {
|
|
978
|
+
const hash = createHash2("sha256");
|
|
979
|
+
const files = [];
|
|
980
|
+
for (const filePath of await listFilesRecursive(skill.directoryPath, skill.directoryPath, true)) {
|
|
981
|
+
const relative = path.relative(skill.directoryPath, filePath);
|
|
982
|
+
const bytes = await fs.readFile(filePath);
|
|
983
|
+
hash.update(relative);
|
|
984
|
+
hash.update("\0");
|
|
985
|
+
hash.update(bytes);
|
|
986
|
+
hash.update("\0");
|
|
987
|
+
files.push({ path: relative.split(path.sep).join("/"), content_b64: bytes.toString("base64") });
|
|
988
|
+
}
|
|
989
|
+
if (hash.digest("hex") !== skill.folderHash) throw new Error("Local Skill files changed before upload. Sync again with the current files.");
|
|
990
|
+
return files;
|
|
991
|
+
}
|
|
992
|
+
async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PATHS, options = {}) {
|
|
993
|
+
if (isNativeSkill(skill)) {
|
|
994
|
+
assertNativePull({
|
|
995
|
+
skills: [{ ...skill, name: "native-" + skill.id }],
|
|
996
|
+
skill_sync_protocol: 2,
|
|
997
|
+
sync_scope: skill.native_scope,
|
|
998
|
+
sync_enabled: true,
|
|
999
|
+
last_synced_at: null
|
|
1000
|
+
});
|
|
1001
|
+
if (bundleBytes?.length) throw new Error("Native Skills require their verified complete file response.");
|
|
1002
|
+
}
|
|
701
1003
|
const skillDir = path.join(
|
|
702
1004
|
paths.skillsDir,
|
|
703
1005
|
safeName(skill.name, paths.skillsDir)
|
|
704
1006
|
);
|
|
1007
|
+
const preserve = {
|
|
1008
|
+
previouslyApplied: options.previouslyApplied,
|
|
1009
|
+
expectedFolderHash: options.expectedFolderHash,
|
|
1010
|
+
native: isNativeSkill(skill),
|
|
1011
|
+
// An unchanged legacy folder upgrading in place: shared files take the cloud version.
|
|
1012
|
+
legacyBaseline: isNativeSkill(skill) && options.legacyBaseline === true && options.expectedFolderHash !== void 0
|
|
1013
|
+
};
|
|
705
1014
|
if (bundleBytes?.length) {
|
|
706
1015
|
const bundlePath = path.join(
|
|
707
1016
|
os.tmpdir(),
|
|
@@ -709,8 +1018,11 @@ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PA
|
|
|
709
1018
|
);
|
|
710
1019
|
try {
|
|
711
1020
|
await fs.writeFile(bundlePath, bundleBytes);
|
|
712
|
-
|
|
713
|
-
|
|
1021
|
+
let appliedFiles = [];
|
|
1022
|
+
await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
|
|
1023
|
+
appliedFiles = await extractZipToDirectory(bundlePath, stagingDir);
|
|
1024
|
+
}, preserve);
|
|
1025
|
+
return { written: true, appliedFiles };
|
|
714
1026
|
} finally {
|
|
715
1027
|
await fs.rm(bundlePath, { force: true });
|
|
716
1028
|
}
|
|
@@ -724,6 +1036,7 @@ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PA
|
|
|
724
1036
|
`Synced bundle for "${skill.name}" is missing SKILL.md or SKILLS.md`
|
|
725
1037
|
);
|
|
726
1038
|
}
|
|
1039
|
+
const appliedFiles = [];
|
|
727
1040
|
await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
|
|
728
1041
|
for (const bundleFile of skill.bundle_files || []) {
|
|
729
1042
|
const filePath = resolveSkillBundleFilePath(
|
|
@@ -735,9 +1048,24 @@ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PA
|
|
|
735
1048
|
filePath,
|
|
736
1049
|
Buffer.from(bundleFile.content_b64, "base64")
|
|
737
1050
|
);
|
|
1051
|
+
appliedFiles.push(toPosixRelativePath(stagingDir, filePath));
|
|
738
1052
|
}
|
|
739
|
-
|
|
740
|
-
|
|
1053
|
+
if (skill.native_reference) {
|
|
1054
|
+
const identity = {
|
|
1055
|
+
schema: 1,
|
|
1056
|
+
cloudId: skill.id,
|
|
1057
|
+
reference: skill.native_reference,
|
|
1058
|
+
displayName: skill.display_name || skill.name,
|
|
1059
|
+
frontmatterName: nativeFrontmatterName(skill.skill_md || ""),
|
|
1060
|
+
scope: skill.native_scope,
|
|
1061
|
+
folderHash: await computeFolderHash(stagingDir),
|
|
1062
|
+
files: Object.fromEntries((skill.bundle_files || []).map((file) => [file.path, nativeFileDigest(Buffer.from(file.content_b64, "base64"))]))
|
|
1063
|
+
};
|
|
1064
|
+
await fs.writeFile(path.join(stagingDir, NATIVE_IDENTITY_FILE), JSON.stringify(identity), { mode: 384 });
|
|
1065
|
+
appliedFiles.push(NATIVE_IDENTITY_FILE);
|
|
1066
|
+
}
|
|
1067
|
+
}, preserve);
|
|
1068
|
+
return { written: true, appliedFiles: appliedFiles.sort() };
|
|
741
1069
|
}
|
|
742
1070
|
await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
|
|
743
1071
|
await fs.writeFile(
|
|
@@ -745,13 +1073,15 @@ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PA
|
|
|
745
1073
|
skill.skill_md ?? "",
|
|
746
1074
|
"utf8"
|
|
747
1075
|
);
|
|
748
|
-
});
|
|
749
|
-
return true;
|
|
1076
|
+
}, preserve);
|
|
1077
|
+
return { written: true, appliedFiles: ["SKILL.md"] };
|
|
750
1078
|
}
|
|
751
1079
|
|
|
752
1080
|
// src/runtime/skill-sync/cloud-client.ts
|
|
1081
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
753
1082
|
async function requestJson(url, jwt, options = {}) {
|
|
754
1083
|
const response = await fetch(url, {
|
|
1084
|
+
signal: AbortSignal.timeout(9e4),
|
|
755
1085
|
method: options.method || "POST",
|
|
756
1086
|
headers: {
|
|
757
1087
|
"Content-Type": "application/json",
|
|
@@ -772,7 +1102,7 @@ async function fetchSyncSettings(serverUrl, jwt) {
|
|
|
772
1102
|
}
|
|
773
1103
|
async function pullSkills(serverUrl, jwt) {
|
|
774
1104
|
return requestJson(`${serverUrl}/portal_skills/sync-pull`, jwt, {
|
|
775
|
-
body: {}
|
|
1105
|
+
body: { skill_sync_protocol: 2 }
|
|
776
1106
|
});
|
|
777
1107
|
}
|
|
778
1108
|
async function pushChangedSkills(serverUrl, jwt, changedSkills) {
|
|
@@ -782,28 +1112,55 @@ async function pushChangedSkills(serverUrl, jwt, changedSkills) {
|
|
|
782
1112
|
skill_md: skill.skillMd,
|
|
783
1113
|
source_url: skill.sourceUrl,
|
|
784
1114
|
folder_hash: skill.folderHash,
|
|
785
|
-
bundle_base64: await createSkillBundleBase64(skill)
|
|
1115
|
+
...skill.nativeReference ? { bundle_files: await captureNativeSkillFiles(skill) } : { bundle_base64: await createSkillBundleBase64(skill) },
|
|
1116
|
+
...skill.cloudId ? { cloud_id: skill.cloudId } : {},
|
|
1117
|
+
...skill.nativeReference ? {
|
|
1118
|
+
native_reference: skill.nativeReference,
|
|
1119
|
+
display_name: skill.displayName,
|
|
1120
|
+
native_scope: skill.nativeScope
|
|
1121
|
+
} : {}
|
|
786
1122
|
})));
|
|
787
1123
|
return requestJson(`${serverUrl}/portal_skills/sync-push`, jwt, {
|
|
788
1124
|
body: {
|
|
1125
|
+
skill_sync_protocol: 2,
|
|
789
1126
|
skills: payloadSkills
|
|
790
1127
|
}
|
|
791
1128
|
});
|
|
792
1129
|
}
|
|
793
1130
|
async function downloadSkillBundle(bundleUrl) {
|
|
794
|
-
const response = await fetch(bundleUrl);
|
|
1131
|
+
const response = await fetch(bundleUrl, { signal: AbortSignal.timeout(9e4) });
|
|
795
1132
|
if (!response.ok) {
|
|
796
1133
|
const text = await response.text();
|
|
797
1134
|
throw new Error(`GET ${bundleUrl} \u2192 ${response.status}: ${text}`);
|
|
798
1135
|
}
|
|
799
1136
|
return Buffer.from(await response.arrayBuffer());
|
|
800
1137
|
}
|
|
801
|
-
async function updateAgentTargets(serverUrl, jwt, skillId, targets) {
|
|
1138
|
+
async function updateAgentTargets(serverUrl, jwt, skillId, targets, expectedUpdatedAt, native) {
|
|
1139
|
+
if (native) {
|
|
1140
|
+
checkedNativeReference(native.reference, skillId);
|
|
1141
|
+
if (!Number.isSafeInteger(native.settingsRevision) || native.settingsRevision < 0) throw new Error("Refresh native Skill settings before changing assignments.");
|
|
1142
|
+
const payload = {
|
|
1143
|
+
operation: "update",
|
|
1144
|
+
target: { skill_id: skillId, access_revision: native.reference.access_revision },
|
|
1145
|
+
revision: native.settingsRevision,
|
|
1146
|
+
patch: { agent_targets: Object.fromEntries(Object.entries(targets).sort(([a], [b]) => a.localeCompare(b))) }
|
|
1147
|
+
};
|
|
1148
|
+
const requestId = "sync-settings-" + createHash3("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
1149
|
+
const saved = await requestJson(
|
|
1150
|
+
`${serverUrl}/portal_skills/native-settings`,
|
|
1151
|
+
jwt,
|
|
1152
|
+
{ body: { ...payload, request_id: requestId } }
|
|
1153
|
+
);
|
|
1154
|
+
const current = saved.current || saved;
|
|
1155
|
+
if (saved.skill_id !== skillId || typeof saved.receipt_id !== "string" || !saved.receipt_id || current.target?.skill_id !== skillId || !current.settings) throw new Error("Native assignment update was not confirmed.");
|
|
1156
|
+
return { success: true, agent_targets: current.settings.agent_targets, updated_at: current.settings.updated_at };
|
|
1157
|
+
}
|
|
802
1158
|
return requestJson(`${serverUrl}/portal_skills/agent-targets`, jwt, {
|
|
803
1159
|
method: "PATCH",
|
|
804
1160
|
body: {
|
|
805
1161
|
skill_id: skillId,
|
|
806
|
-
agent_targets: targets
|
|
1162
|
+
agent_targets: targets,
|
|
1163
|
+
...expectedUpdatedAt ? { expected_updated_at: expectedUpdatedAt } : {}
|
|
807
1164
|
}
|
|
808
1165
|
});
|
|
809
1166
|
}
|
|
@@ -819,6 +1176,18 @@ var EXTERNAL_AGENT_SKILL_DIRS = {
|
|
|
819
1176
|
codex: path2.join(HOME_DIR2, ".codex", "skills")
|
|
820
1177
|
};
|
|
821
1178
|
var EXTERNAL_AGENTS = Object.keys(EXTERNAL_AGENT_SKILL_DIRS);
|
|
1179
|
+
var AGENT_FAILURE_LABELS = {
|
|
1180
|
+
claude_code: "Claude Code",
|
|
1181
|
+
cursor: "Cursor",
|
|
1182
|
+
codex: "Codex",
|
|
1183
|
+
legacy: "legacy ~/.agents/skills"
|
|
1184
|
+
};
|
|
1185
|
+
function agentFailureLabel(agent) {
|
|
1186
|
+
return AGENT_FAILURE_LABELS[agent] ?? agent;
|
|
1187
|
+
}
|
|
1188
|
+
function agentFolderFailureName(agent) {
|
|
1189
|
+
return `${agentFailureLabel(agent)} skills folder`;
|
|
1190
|
+
}
|
|
822
1191
|
async function removeForeignAccountSymlinks(skillsDir, options = {}) {
|
|
823
1192
|
const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
|
|
824
1193
|
const currentRoot = path2.resolve(skillsDir);
|
|
@@ -879,8 +1248,9 @@ async function isManagedSymlink(linkPath, managedRoots) {
|
|
|
879
1248
|
const target = await fs2.readlink(linkPath);
|
|
880
1249
|
const resolvedTarget = path2.resolve(path2.dirname(linkPath), target);
|
|
881
1250
|
return managedRoots.some((root) => resolvedTarget === root || resolvedTarget.startsWith(`${root}${path2.sep}`));
|
|
882
|
-
} catch {
|
|
883
|
-
return false;
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
if (error?.code === "ENOENT") return false;
|
|
1253
|
+
throw error;
|
|
884
1254
|
}
|
|
885
1255
|
}
|
|
886
1256
|
async function ensureCorrectSymlink(linkPath, targetPath) {
|
|
@@ -896,7 +1266,8 @@ async function ensureCorrectSymlink(linkPath, targetPath) {
|
|
|
896
1266
|
} else {
|
|
897
1267
|
return "blocked";
|
|
898
1268
|
}
|
|
899
|
-
} catch {
|
|
1269
|
+
} catch (error) {
|
|
1270
|
+
if (error?.code !== "ENOENT") throw error;
|
|
900
1271
|
}
|
|
901
1272
|
const relativePath = path2.relative(path2.dirname(linkPath), targetPath);
|
|
902
1273
|
await fs2.symlink(relativePath, linkPath);
|
|
@@ -908,19 +1279,26 @@ function defaultAgentSkillDirs(skillsDir) {
|
|
|
908
1279
|
...EXTERNAL_AGENT_SKILL_DIRS
|
|
909
1280
|
};
|
|
910
1281
|
}
|
|
911
|
-
async function removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots) {
|
|
1282
|
+
async function removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots, failures, agent) {
|
|
912
1283
|
let removed = 0;
|
|
913
1284
|
try {
|
|
914
1285
|
await fs2.mkdir(agentDir, { recursive: true });
|
|
915
1286
|
const existingEntries = await fs2.readdir(agentDir, { withFileTypes: true });
|
|
916
1287
|
for (const entry of existingEntries) {
|
|
917
1288
|
const entryPath = path2.join(agentDir, entry.name);
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1289
|
+
try {
|
|
1290
|
+
if (!desiredSkills.has(entry.name) && await isManagedSymlink(entryPath, managedRoots)) {
|
|
1291
|
+
await fs2.unlink(entryPath);
|
|
1292
|
+
removed += 1;
|
|
1293
|
+
}
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
if (error?.code !== "ENOENT") {
|
|
1296
|
+
failures.push({ name: entry.name, error: `${agentFailureLabel(agent)}: could not remove skill link (${error.message})` });
|
|
1297
|
+
}
|
|
921
1298
|
}
|
|
922
1299
|
}
|
|
923
|
-
} catch {
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
failures.push({ name: agentFolderFailureName(agent), error: `Could not read agent skills directory (${error.message})` });
|
|
924
1302
|
}
|
|
925
1303
|
return removed;
|
|
926
1304
|
}
|
|
@@ -965,7 +1343,7 @@ async function detectDeletedAgentSymlinks(cloudSkills, previousState, skillsDir
|
|
|
965
1343
|
continue;
|
|
966
1344
|
}
|
|
967
1345
|
const previous = previousState.skills[skill.name];
|
|
968
|
-
if (!previous) {
|
|
1346
|
+
if (!previous || previous.cloudId !== skill.id || previous.verifiedAgentLinks?.[agent] !== true || !skill.updated_at || previous.cloudUpdatedAt !== skill.updated_at) {
|
|
969
1347
|
continue;
|
|
970
1348
|
}
|
|
971
1349
|
const cloudTargets = normalizeAgentTargets(skill.agent_targets);
|
|
@@ -996,7 +1374,9 @@ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, option
|
|
|
996
1374
|
const result = {
|
|
997
1375
|
linked: 0,
|
|
998
1376
|
removed: 0,
|
|
999
|
-
skipped: 0
|
|
1377
|
+
skipped: 0,
|
|
1378
|
+
verifiedAgentLinks: {},
|
|
1379
|
+
failures: []
|
|
1000
1380
|
};
|
|
1001
1381
|
const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
|
|
1002
1382
|
const legacyGlobalSkillsDir = options.legacyGlobalSkillsDir || LEGACY_AGENTS_SKILLS_DIR;
|
|
@@ -1031,21 +1411,34 @@ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, option
|
|
|
1031
1411
|
result.skipped += desiredByAgent[agent].size;
|
|
1032
1412
|
continue;
|
|
1033
1413
|
}
|
|
1034
|
-
|
|
1414
|
+
try {
|
|
1415
|
+
await fs2.mkdir(agentDir, { recursive: true });
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
result.failures.push({ name: agentFolderFailureName(agent), error: `Could not create agent skills directory (${error.message})` });
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1035
1420
|
const desiredSkills = desiredByAgent[agent];
|
|
1036
1421
|
if (options.removeUndesired !== false) {
|
|
1037
|
-
result.removed += await removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots);
|
|
1422
|
+
result.removed += await removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots, result.failures, agent);
|
|
1038
1423
|
}
|
|
1039
1424
|
for (const skillName of desiredSkills) {
|
|
1040
1425
|
const targetPath = path2.join(skillsDir, skillName);
|
|
1041
1426
|
const linkPath = path2.join(agentDir, safeName(skillName, agentDir));
|
|
1042
1427
|
try {
|
|
1043
|
-
await fs2.
|
|
1428
|
+
if (!(await fs2.stat(path2.join(targetPath, "SKILL.md"))).isFile()) throw new Error("Missing SKILL.md");
|
|
1044
1429
|
} catch {
|
|
1045
1430
|
result.skipped += 1;
|
|
1431
|
+
result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: SKILL.md is missing or unreadable` });
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
let syncOutcome;
|
|
1435
|
+
try {
|
|
1436
|
+
syncOutcome = await ensureCorrectSymlink(linkPath, targetPath);
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
result.skipped += 1;
|
|
1439
|
+
result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: could not create skill link (${error.message})` });
|
|
1046
1440
|
continue;
|
|
1047
1441
|
}
|
|
1048
|
-
const syncOutcome = await ensureCorrectSymlink(linkPath, targetPath);
|
|
1049
1442
|
if (syncOutcome === "linked") {
|
|
1050
1443
|
result.linked += 1;
|
|
1051
1444
|
} else if (syncOutcome === "blocked") {
|
|
@@ -1053,9 +1446,13 @@ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, option
|
|
|
1053
1446
|
`[skill-sync] Could not link "${skillName}" for ${agent}: non-symlink entry blocks ${linkPath}`
|
|
1054
1447
|
);
|
|
1055
1448
|
result.skipped += 1;
|
|
1449
|
+
result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: an existing file or folder blocks the skill link` });
|
|
1056
1450
|
} else {
|
|
1057
1451
|
result.skipped += 1;
|
|
1058
1452
|
}
|
|
1453
|
+
if (syncOutcome !== "blocked") {
|
|
1454
|
+
result.verifiedAgentLinks[skillName] = { ...result.verifiedAgentLinks[skillName], [agent]: true };
|
|
1455
|
+
}
|
|
1059
1456
|
}
|
|
1060
1457
|
}
|
|
1061
1458
|
if (options.removeUndesired !== false && !Object.values(agentSkillDirs).some(
|
|
@@ -1064,26 +1461,46 @@ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, option
|
|
|
1064
1461
|
result.removed += await removeUndesiredManagedSymlinks(
|
|
1065
1462
|
legacyGlobalSkillsDir,
|
|
1066
1463
|
/* @__PURE__ */ new Set(),
|
|
1067
|
-
managedRoots
|
|
1464
|
+
managedRoots,
|
|
1465
|
+
result.failures,
|
|
1466
|
+
"legacy"
|
|
1068
1467
|
);
|
|
1069
1468
|
}
|
|
1070
1469
|
return result;
|
|
1071
1470
|
}
|
|
1072
1471
|
|
|
1073
1472
|
// src/runtime/skill-sync/sync-plan.ts
|
|
1074
|
-
function
|
|
1473
|
+
function selectCloudSkillsToApply(skills) {
|
|
1474
|
+
const winners = /* @__PURE__ */ new Map();
|
|
1475
|
+
for (const skill of skills) {
|
|
1476
|
+
const current = winners.get(skill.name);
|
|
1477
|
+
if (!current || (skill.updated_at || "") >= (current.updated_at || "")) {
|
|
1478
|
+
winners.set(skill.name, skill);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return [...winners.values()];
|
|
1482
|
+
}
|
|
1483
|
+
function getPushCandidates(localSkills, syncState, cloudCuratedSkillNames = /* @__PURE__ */ new Set(), cloudSkillNames, cloudSkills = []) {
|
|
1075
1484
|
return localSkills.filter((skill) => {
|
|
1076
1485
|
if (cloudCuratedSkillNames.has(skill.name)) {
|
|
1077
1486
|
return false;
|
|
1078
1487
|
}
|
|
1079
|
-
const previous = syncState.skills[skill.name];
|
|
1488
|
+
const previous = syncState.skills[skill.name] || (skill.cloudId ? Object.values(syncState.skills).find((row) => row.cloudId === skill.cloudId) : void 0);
|
|
1489
|
+
const native = cloudSkills.find((row) => isNativeSkill(row) && row.id === (skill.cloudId || previous?.cloudId));
|
|
1490
|
+
if (native || skill.nativeReference || previous?.nativeReference || previous?.nativeUnavailable) {
|
|
1491
|
+
return Boolean(native && skill.nativeReference && skill.nativeScope === native.native_scope && (previous ? previous.folderHash !== skill.folderHash : skill.nativeBaseFolderHash !== skill.folderHash));
|
|
1492
|
+
}
|
|
1080
1493
|
if (!previous) {
|
|
1494
|
+
if (hasNativeFolderHistory(syncState, skill.name) || /^native-[a-f0-9-]{36}$/.test(skill.name)) return false;
|
|
1081
1495
|
return true;
|
|
1082
1496
|
}
|
|
1083
1497
|
if (cloudSkillNames && !cloudSkillNames.has(skill.name)) {
|
|
1084
1498
|
return false;
|
|
1085
1499
|
}
|
|
1086
1500
|
return previous.folderHash !== skill.folderHash;
|
|
1501
|
+
}).map((skill) => {
|
|
1502
|
+
const previous = syncState.skills[skill.name] || Object.values(syncState.skills).find((row) => row.cloudId === skill.cloudId);
|
|
1503
|
+
return { ...skill, ...skill.cloudId || previous?.cloudId ? { cloudId: skill.cloudId || previous.cloudId } : {} };
|
|
1087
1504
|
});
|
|
1088
1505
|
}
|
|
1089
1506
|
|
|
@@ -1094,7 +1511,7 @@ async function writeCloudSkillWithBundleFallback(skill, dependencies) {
|
|
|
1094
1511
|
const bundleBytes = await dependencies.downloadSkillBundle(skill.skill_source_url);
|
|
1095
1512
|
const wroteBundleToDisk = await dependencies.writeCloudSkillToDisk(skill, bundleBytes);
|
|
1096
1513
|
if (wroteBundleToDisk) {
|
|
1097
|
-
return
|
|
1514
|
+
return wroteBundleToDisk;
|
|
1098
1515
|
}
|
|
1099
1516
|
dependencies.onWarning?.(
|
|
1100
1517
|
`Bundle sync for "${skill.name}" produced no local changes, falling back to SKILL.md payload.`,
|
|
@@ -1126,7 +1543,7 @@ async function writeCloudSkillWithBundleFallback(skill, dependencies) {
|
|
|
1126
1543
|
}
|
|
1127
1544
|
|
|
1128
1545
|
// src/runtime/skill-sync/index.ts
|
|
1129
|
-
var BASE_SKILL_NAMES = /* @__PURE__ */ new Set(["notis-
|
|
1546
|
+
var BASE_SKILL_NAMES = /* @__PURE__ */ new Set(["notis-spaces", "notis-query", "notis-cli"]);
|
|
1130
1547
|
function withoutBaseSkills(pullResponse) {
|
|
1131
1548
|
return {
|
|
1132
1549
|
...pullResponse,
|
|
@@ -1159,6 +1576,62 @@ var DEFAULT_RUN_SKILL_SYNC_DEPS = {
|
|
|
1159
1576
|
removeForeignAccountSymlinks,
|
|
1160
1577
|
updateAgentTargets
|
|
1161
1578
|
};
|
|
1579
|
+
var MIRROR_ONLY_DEPS = {
|
|
1580
|
+
removeForeignAccountSymlinks: async () => 0,
|
|
1581
|
+
removeAllSymlinksForSkill: async () => 0,
|
|
1582
|
+
syncSymlinks: async () => ({ linked: 0, removed: 0, skipped: 0, verifiedAgentLinks: {} })
|
|
1583
|
+
};
|
|
1584
|
+
async function forgetOtherAccountsAgentLinks(paths) {
|
|
1585
|
+
const usersDir = dirname(dirname(paths.syncStatePath));
|
|
1586
|
+
let entries;
|
|
1587
|
+
try {
|
|
1588
|
+
entries = await fs3.readdir(usersDir, { withFileTypes: true });
|
|
1589
|
+
} catch {
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
for (const entry of entries) {
|
|
1593
|
+
if (!entry.isDirectory()) continue;
|
|
1594
|
+
const statePath = join(usersDir, entry.name, ".notis-sync.json");
|
|
1595
|
+
if (statePath === paths.syncStatePath) continue;
|
|
1596
|
+
let state = null;
|
|
1597
|
+
try {
|
|
1598
|
+
state = JSON.parse(await fs3.readFile(statePath, "utf8"));
|
|
1599
|
+
} catch {
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
if (!state || typeof state !== "object" || !state.skills || typeof state.skills !== "object") continue;
|
|
1603
|
+
let changed = false;
|
|
1604
|
+
for (const skill of Object.values(state.skills)) {
|
|
1605
|
+
if (skill && typeof skill === "object" && "verifiedAgentLinks" in skill) {
|
|
1606
|
+
delete skill.verifiedAgentLinks;
|
|
1607
|
+
changed = true;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
if (!changed) continue;
|
|
1611
|
+
const temporaryPath = `${statePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1612
|
+
await fs3.writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}
|
|
1613
|
+
`, "utf8");
|
|
1614
|
+
await fs3.rename(temporaryPath, statePath);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
function isolatedLinkOptions(paths) {
|
|
1618
|
+
if (!paths.isolatedNamespace && !paths.homeDir) return void 0;
|
|
1619
|
+
const root = paths.isolatedNamespace ? join(paths.syncRoot, "environments", paths.isolatedNamespace, "agents") : paths.homeDir;
|
|
1620
|
+
return {
|
|
1621
|
+
agentSkillDirs: {
|
|
1622
|
+
notis: paths.skillsDir,
|
|
1623
|
+
claude_code: join(root, ".claude", "skills"),
|
|
1624
|
+
cursor: join(root, ".cursor", "skills"),
|
|
1625
|
+
codex: join(root, ".codex", "skills")
|
|
1626
|
+
},
|
|
1627
|
+
legacyGlobalSkillsDir: join(root, ".agents", "skills")
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
function assertSyncEnvironment(pull, settings) {
|
|
1631
|
+
if ((pull.sync_namespace || null) !== (settings.sync_namespace || null) || settings.sync_scope !== void 0 && pull.sync_scope !== settings.sync_scope) {
|
|
1632
|
+
throw new Error("Skill sync environment changed. Existing mirrors were retained.");
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1162
1635
|
function toSkillMap(skills) {
|
|
1163
1636
|
return new Map(skills.map((skill) => [skill.name, skill]));
|
|
1164
1637
|
}
|
|
@@ -1174,6 +1647,15 @@ function decodeJwtSubject(jwt) {
|
|
|
1174
1647
|
return null;
|
|
1175
1648
|
}
|
|
1176
1649
|
}
|
|
1650
|
+
function cloudContentHash(skill) {
|
|
1651
|
+
return createHash4("sha256").update(JSON.stringify({
|
|
1652
|
+
md: skill.skill_md,
|
|
1653
|
+
hash: skill.skill_folder_hash,
|
|
1654
|
+
source: skill.skill_source_url,
|
|
1655
|
+
files: skill.bundle_files?.slice().sort((a, b) => a.path.localeCompare(b.path)),
|
|
1656
|
+
hydrationFailed: skill.bundle_hydration_failed === true
|
|
1657
|
+
})).digest("hex");
|
|
1658
|
+
}
|
|
1177
1659
|
function shouldWriteCloudSkill(cloudSkill, localSkills, previousState) {
|
|
1178
1660
|
const skillName = cloudSkill.name;
|
|
1179
1661
|
const cloudHash = cloudSkill.skill_folder_hash || "";
|
|
@@ -1181,40 +1663,99 @@ function shouldWriteCloudSkill(cloudSkill, localSkills, previousState) {
|
|
|
1181
1663
|
if (!localSkill) {
|
|
1182
1664
|
return true;
|
|
1183
1665
|
}
|
|
1666
|
+
if (isNativeSkill(cloudSkill)) {
|
|
1667
|
+
if (localSkill.nativeScope && localSkill.nativeScope !== cloudSkill.native_scope) return false;
|
|
1668
|
+
if (nativeLocalReady(cloudSkill, localSkill)) return false;
|
|
1669
|
+
const nativePrevious = previousNativeState(previousState, cloudSkill);
|
|
1670
|
+
return localSkill.nativeFilesCurrent === true && localSkill.folderHash === localSkill.nativeBaseFolderHash || !localSkill.nativeReference && nativePrevious?.cloudId === cloudSkill.id && nativePrevious.folderHash === localSkill.folderHash;
|
|
1671
|
+
}
|
|
1672
|
+
const previous = previousState.skills[skillName];
|
|
1673
|
+
if (previous?.folderHash === localSkill.folderHash && previous.cloudContentHash === cloudContentHash(cloudSkill)) return false;
|
|
1674
|
+
if (previous && previous.cloudContentHash === void 0 && previous.cloudId === cloudSkill.id && previous.appliedCloudFolderHash !== void 0 && previous.appliedCloudFolderHash === cloudHash && previous.cloudUpdatedAt === cloudSkill.updated_at && previous.folderHash === localSkill.folderHash) {
|
|
1675
|
+
return false;
|
|
1676
|
+
}
|
|
1184
1677
|
if (cloudSkill.source === "curated") {
|
|
1185
1678
|
return cloudHash ? cloudHash !== localSkill.folderHash : true;
|
|
1186
1679
|
}
|
|
1187
|
-
const previous = previousState.skills[skillName];
|
|
1188
1680
|
const localChangedSinceLastSync = !previous || previous.folderHash !== localSkill.folderHash;
|
|
1189
|
-
return !localChangedSinceLastSync && Boolean(cloudHash) && cloudHash !== localSkill.folderHash;
|
|
1681
|
+
return !localChangedSinceLastSync && (Boolean(cloudHash) && cloudHash !== localSkill.folderHash || Boolean(previous?.cloudContentHash && previous.cloudContentHash !== cloudContentHash(cloudSkill)));
|
|
1682
|
+
}
|
|
1683
|
+
function collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames) {
|
|
1684
|
+
const applied = {};
|
|
1685
|
+
for (const skill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
1686
|
+
applied[skill.name] = writtenSkillNames.has(skill.name) ? skill.skill_folder_hash || "" : previousState.skills[skill.name]?.appliedCloudFolderHash;
|
|
1687
|
+
}
|
|
1688
|
+
return applied;
|
|
1689
|
+
}
|
|
1690
|
+
function collectAppliedFiles(pullResponse, previousState, appliedFilesByName) {
|
|
1691
|
+
const applied = {};
|
|
1692
|
+
for (const skill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
1693
|
+
applied[skill.name] = appliedFilesByName[skill.name] ?? previousState.skills[skill.name]?.appliedFiles;
|
|
1694
|
+
}
|
|
1695
|
+
return applied;
|
|
1190
1696
|
}
|
|
1191
|
-
function buildSyncState(pullResponse, localSkills, lastSyncedAt) {
|
|
1697
|
+
function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLinks = {}, failedContentNames = /* @__PURE__ */ new Set(), appliedRevisions = {}, appliedFiles = {}, previousState = { version: 1, lastSyncedAt: null, skills: {} }) {
|
|
1192
1698
|
const localSkillMap = toSkillMap(localSkills);
|
|
1193
1699
|
const skills = Object.fromEntries(
|
|
1194
|
-
pullResponse.skills.map((skill) => {
|
|
1700
|
+
selectCloudSkillsToApply(pullResponse.skills).map((skill) => {
|
|
1195
1701
|
const localSkill = localSkillMap.get(skill.name);
|
|
1702
|
+
const appliedCloudFolderHash = appliedRevisions[skill.name];
|
|
1703
|
+
const appliedFileList = appliedFiles[skill.name];
|
|
1704
|
+
if (pendingLegacyUpgrade(skill, localSkill, previousState)) {
|
|
1705
|
+
const { nativeUnavailable: _unavailable, ...previous } = previousNativeState(previousState, skill);
|
|
1706
|
+
return [skill.name, { ...previous, verifiedAgentLinks: verifiedAgentLinks[skill.name] ?? {} }];
|
|
1707
|
+
}
|
|
1708
|
+
if (isNativeSkill(skill) && !nativeLocalReady(skill, localSkill)) {
|
|
1709
|
+
const previous = previousNativeState(previousState, skill);
|
|
1710
|
+
return [skill.name, { ...previous || {
|
|
1711
|
+
cloudId: skill.id,
|
|
1712
|
+
folderHash: localSkill?.nativeBaseFolderHash || "",
|
|
1713
|
+
agentTargets: normalizeAgentTargets(skill.agent_targets),
|
|
1714
|
+
syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1715
|
+
...localSkill?.nativeReference ? { nativeReference: localSkill.nativeReference, nativeScope: localSkill.nativeScope } : {}
|
|
1716
|
+
}, nativeUnavailable: true, verifiedAgentLinks: {} }];
|
|
1717
|
+
}
|
|
1196
1718
|
return [
|
|
1197
1719
|
skill.name,
|
|
1198
1720
|
{
|
|
1199
1721
|
cloudId: skill.id,
|
|
1722
|
+
...nativeState(skill),
|
|
1200
1723
|
folderHash: localSkill?.folderHash || skill.skill_folder_hash || "",
|
|
1201
1724
|
agentTargets: normalizeAgentTargets(skill.agent_targets),
|
|
1725
|
+
verifiedAgentLinks: skill.status === "active" ? verifiedAgentLinks[skill.name] ?? {} : {},
|
|
1726
|
+
cloudUpdatedAt: skill.updated_at,
|
|
1727
|
+
...!failedContentNames.has(skill.name) && !skill.skill_source_url ? { cloudContentHash: cloudContentHash(skill) } : {},
|
|
1728
|
+
...appliedCloudFolderHash !== void 0 ? { appliedCloudFolderHash } : {},
|
|
1729
|
+
...appliedFileList !== void 0 ? { appliedFiles: appliedFileList } : {},
|
|
1202
1730
|
syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
1203
1731
|
}
|
|
1204
1732
|
];
|
|
1205
1733
|
})
|
|
1206
1734
|
);
|
|
1207
|
-
|
|
1735
|
+
for (const local of localSkills) {
|
|
1736
|
+
if (!local.nativeReference || Object.values(skills).some((row) => row.cloudId === local.cloudId)) continue;
|
|
1737
|
+
skills[local.name] = {
|
|
1738
|
+
cloudId: local.cloudId,
|
|
1739
|
+
nativeReference: local.nativeReference,
|
|
1740
|
+
nativeScope: local.nativeScope,
|
|
1741
|
+
nativeUnavailable: true,
|
|
1742
|
+
verifiedAgentLinks: {},
|
|
1743
|
+
folderHash: local.nativeBaseFolderHash || "",
|
|
1744
|
+
agentTargets: normalizeAgentTargets(),
|
|
1745
|
+
syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
return retainMissingNativeState(previousState, {
|
|
1208
1749
|
version: 1,
|
|
1209
1750
|
lastSyncedAt,
|
|
1210
1751
|
skills
|
|
1211
|
-
};
|
|
1752
|
+
});
|
|
1212
1753
|
}
|
|
1213
1754
|
function buildLocalSymlinkCandidates(pullResponse, localSkills, previousState) {
|
|
1214
1755
|
const cloudSkillNames = new Set(
|
|
1215
1756
|
pullResponse.skills.map((skill) => skill.name)
|
|
1216
1757
|
);
|
|
1217
|
-
const localOnlySkills = localSkills.filter((skill) => !cloudSkillNames.has(skill.name)).map((skill) => {
|
|
1758
|
+
const localOnlySkills = localSkills.filter((skill) => !cloudSkillNames.has(skill.name)).filter((skill) => !skill.nativeReference && !previousState.skills[skill.name]?.nativeReference && !previousState.skills[skill.name]?.nativeUnavailable && !hasNativeFolderHistory(previousState, skill.name) && !/^native-[a-f0-9-]{36}$/.test(skill.name)).map((skill) => {
|
|
1218
1759
|
const previous = previousState.skills[skill.name];
|
|
1219
1760
|
return {
|
|
1220
1761
|
id: previous?.cloudId || `local-${skill.name}`,
|
|
@@ -1227,7 +1768,7 @@ function buildLocalSymlinkCandidates(pullResponse, localSkills, previousState) {
|
|
|
1227
1768
|
status: "active"
|
|
1228
1769
|
};
|
|
1229
1770
|
});
|
|
1230
|
-
return [...pullResponse
|
|
1771
|
+
return [...nativeLinkRows(pullResponse, localSkills, previousState), ...localOnlySkills];
|
|
1231
1772
|
}
|
|
1232
1773
|
function isEmptySyncState(state) {
|
|
1233
1774
|
return state.lastSyncedAt === null && Object.keys(state.skills).length === 0;
|
|
@@ -1254,27 +1795,48 @@ function applyLegacyFirstRunState(localSkills, scopedState, legacyState) {
|
|
|
1254
1795
|
skills: migratedSkills
|
|
1255
1796
|
};
|
|
1256
1797
|
}
|
|
1257
|
-
async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previousState, syncPaths, deps) {
|
|
1798
|
+
async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previousState, syncPaths, deps, failures = [], writtenSkillNames = /* @__PURE__ */ new Set(), appliedFilesByName = {}, acknowledgedNative = /* @__PURE__ */ new Map()) {
|
|
1258
1799
|
const localSkillMap = toSkillMap(localSkills);
|
|
1259
1800
|
const warnSkillSync = (message, error) => {
|
|
1260
1801
|
console.warn(`[Notis] ${message}`, error);
|
|
1261
1802
|
};
|
|
1262
1803
|
let downloaded = 0;
|
|
1263
|
-
for (const cloudSkill of pullResponse.skills) {
|
|
1264
|
-
|
|
1804
|
+
for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
1805
|
+
const local = localSkillMap.get(cloudSkill.name);
|
|
1806
|
+
const acknowledged = isNativeSkill(cloudSkill) && local && acknowledgedNative.get(cloudSkill.id) === local.folderHash;
|
|
1807
|
+
const legacyBaseline = Boolean(local && pendingLegacyUpgrade(cloudSkill, local, previousState) && previousNativeState(previousState, cloudSkill)?.folderHash === local.folderHash);
|
|
1808
|
+
if (!acknowledged && !shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
|
|
1809
|
+
if (isNativeSkill(cloudSkill) && !nativeLocalReady(cloudSkill, local)) {
|
|
1810
|
+
failures.push({ name: cloudSkill.name, error: "Local native Skill changes were retained; resolve the version or identity conflict before using them." });
|
|
1811
|
+
}
|
|
1265
1812
|
continue;
|
|
1266
1813
|
}
|
|
1267
|
-
|
|
1814
|
+
const outcome = await writeCloudSkillWithBundleFallback(cloudSkill, {
|
|
1268
1815
|
downloadSkillBundle: deps.downloadSkillBundle,
|
|
1269
|
-
writeCloudSkillToDisk: (skill, bundleBytes) => deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths
|
|
1816
|
+
writeCloudSkillToDisk: (skill, bundleBytes) => deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths, {
|
|
1817
|
+
// What the last write put there. Everything else in the folder was
|
|
1818
|
+
// produced locally and survives this one.
|
|
1819
|
+
previouslyApplied: previousNativeState(previousState, skill)?.appliedFiles,
|
|
1820
|
+
...isNativeSkill(skill) && local ? { expectedFolderHash: local.folderHash } : {},
|
|
1821
|
+
...legacyBaseline ? { legacyBaseline } : {}
|
|
1822
|
+
}),
|
|
1270
1823
|
onWarning: warnSkillSync
|
|
1271
|
-
})
|
|
1824
|
+
});
|
|
1825
|
+
if (outcome) {
|
|
1272
1826
|
downloaded += 1;
|
|
1827
|
+
writtenSkillNames.add(cloudSkill.name);
|
|
1828
|
+
if (typeof outcome === "object" && Array.isArray(outcome.appliedFiles)) {
|
|
1829
|
+
appliedFilesByName[cloudSkill.name] = outcome.appliedFiles;
|
|
1830
|
+
}
|
|
1831
|
+
} else {
|
|
1832
|
+
failures.push({ name: cloudSkill.name, error: "Skill content could not be downloaded or written; sync will retry" });
|
|
1273
1833
|
}
|
|
1274
1834
|
}
|
|
1275
1835
|
return downloaded;
|
|
1276
1836
|
}
|
|
1277
1837
|
function assertSkillsPullAuthorized(pullResponse) {
|
|
1838
|
+
if (!Array.isArray(pullResponse.skills)) throw new Error("Skill inventory is unavailable; existing files were retained.");
|
|
1839
|
+
assertNativePull(pullResponse);
|
|
1278
1840
|
if (pullResponse.entitlement_access?.code === "entitlement_upgrade_required" && pullResponse.entitlement_access.entitlement === "skills") {
|
|
1279
1841
|
throw new Error(
|
|
1280
1842
|
"Skill sync access was denied; preserving existing local skills."
|
|
@@ -1292,68 +1854,108 @@ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies
|
|
|
1292
1854
|
"Cannot materialize skills without a valid authenticated desktop session."
|
|
1293
1855
|
);
|
|
1294
1856
|
}
|
|
1295
|
-
|
|
1296
|
-
const pullResponse = await deps.pullSkills(serverUrl, jwt);
|
|
1857
|
+
let pullResponse = await deps.pullSkills(serverUrl, jwt);
|
|
1297
1858
|
assertSkillsPullAuthorized(pullResponse);
|
|
1859
|
+
if (options.syncSettings) assertSyncEnvironment(pullResponse, options.syncSettings);
|
|
1860
|
+
const syncPaths = getSkillSyncPathsForUser(options.canonicalUserId?.trim() || authUserId, void 0, pullResponse.sync_namespace);
|
|
1298
1861
|
const previousState = await deps.readSyncState(syncPaths);
|
|
1299
1862
|
const localSkills = await deps.scanLocalSkills(syncPaths);
|
|
1863
|
+
pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
|
|
1864
|
+
const failedDownloads = [];
|
|
1865
|
+
const writtenSkillNames = /* @__PURE__ */ new Set();
|
|
1866
|
+
const appliedFilesByName = {};
|
|
1300
1867
|
const downloaded = await writePulledSkillsToScopedMirror(
|
|
1301
1868
|
pullResponse,
|
|
1302
1869
|
localSkills,
|
|
1303
1870
|
previousState,
|
|
1304
1871
|
syncPaths,
|
|
1305
|
-
deps
|
|
1872
|
+
deps,
|
|
1873
|
+
failedDownloads,
|
|
1874
|
+
writtenSkillNames,
|
|
1875
|
+
appliedFilesByName
|
|
1306
1876
|
);
|
|
1307
1877
|
const finalLocalSkills = await deps.scanLocalSkills(syncPaths);
|
|
1308
1878
|
const lastSyncedAt = pullResponse.last_synced_at || (/* @__PURE__ */ new Date()).toISOString();
|
|
1309
1879
|
const relinkSkillNames = new Set(options.relinkSkillNames || []);
|
|
1880
|
+
const failures = [...failedDownloads];
|
|
1881
|
+
const verifiedLinks = {};
|
|
1882
|
+
for (const skill of pullResponse.skills) {
|
|
1883
|
+
const previous = previousState.skills[skill.name];
|
|
1884
|
+
if (skill.updated_at && previous?.cloudId === skill.id && previous.cloudUpdatedAt === skill.updated_at) {
|
|
1885
|
+
verifiedLinks[skill.name] = { ...previous.verifiedAgentLinks };
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1310
1888
|
if (relinkSkillNames.size > 0) {
|
|
1311
|
-
await deps.syncSymlinks(
|
|
1312
|
-
pullResponse.
|
|
1889
|
+
const relinked = await deps.syncSymlinks(
|
|
1890
|
+
nativeLinkRows(pullResponse, finalLocalSkills, previousState).filter((skill) => relinkSkillNames.has(skill.name)),
|
|
1313
1891
|
syncPaths.skillsDir,
|
|
1314
|
-
{ removeUndesired: false }
|
|
1892
|
+
{ ...isolatedLinkOptions(syncPaths), removeUndesired: false }
|
|
1315
1893
|
);
|
|
1894
|
+
failures.push(...(relinked.failures ?? []).filter(
|
|
1895
|
+
(failure) => !failedDownloads.some((download) => download.name === failure.name)
|
|
1896
|
+
));
|
|
1897
|
+
for (const name of relinkSkillNames) {
|
|
1898
|
+
verifiedLinks[name] = relinked.verifiedAgentLinks?.[name] ?? {};
|
|
1899
|
+
}
|
|
1316
1900
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1901
|
+
for (const failure of failedDownloads) delete verifiedLinks[failure.name];
|
|
1902
|
+
const materializedState = buildSyncState(
|
|
1903
|
+
pullResponse,
|
|
1904
|
+
finalLocalSkills,
|
|
1905
|
+
lastSyncedAt,
|
|
1906
|
+
verifiedLinks,
|
|
1907
|
+
new Set(failedDownloads.map((item) => item.name)),
|
|
1908
|
+
collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
|
|
1909
|
+
collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
|
|
1910
|
+
previousState
|
|
1320
1911
|
);
|
|
1912
|
+
for (const [name, entry] of Object.entries(materializedState.skills)) {
|
|
1913
|
+
if (writtenSkillNames.has(name)) continue;
|
|
1914
|
+
const previous = previousState.skills[name];
|
|
1915
|
+
if (previous) {
|
|
1916
|
+
entry.folderHash = previous.folderHash;
|
|
1917
|
+
entry.cloudContentHash = previous.cloudContentHash;
|
|
1918
|
+
} else {
|
|
1919
|
+
delete materializedState.skills[name];
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
materializedState.skills = { ...previousState.skills, ...materializedState.skills };
|
|
1923
|
+
let removed = 0;
|
|
1924
|
+
for (const [name, previous] of Object.entries(previousState.skills)) {
|
|
1925
|
+
if (!previous.nativeReference && !previous.nativeUnavailable) continue;
|
|
1926
|
+
const current = pullResponse.skills.find((skill) => skill.id === previous.cloudId);
|
|
1927
|
+
const local = current && finalLocalSkills.find((row) => row.name === current.name);
|
|
1928
|
+
if (!current || !nativeLocalReady(current, local) && !pendingLegacyUpgrade(current, local, previousState)) {
|
|
1929
|
+
removed += await deps.removeAllSymlinksForSkill(name, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
await deps.writeSyncState(materializedState, syncPaths);
|
|
1321
1933
|
return {
|
|
1934
|
+
materializedSkillNames: pullResponse.skills.filter((skill) => finalLocalSkills.some((local) => local.name === skill.name) && (!isNativeSkill(skill) || skill.status === "active" && skill.agent_targets?.notis !== false && nativeLocalReady(skill, finalLocalSkills.find((local) => local.name === skill.name))) && !failedDownloads.some((failure) => failure.name === skill.name)).map((skill) => skill.name),
|
|
1322
1935
|
pulled: pullResponse.skills.length,
|
|
1323
1936
|
downloaded,
|
|
1324
1937
|
deleted: 0,
|
|
1325
|
-
removed
|
|
1326
|
-
lastSyncedAt
|
|
1938
|
+
removed,
|
|
1939
|
+
lastSyncedAt,
|
|
1940
|
+
...failures.length ? { failedLinks: failures } : {}
|
|
1327
1941
|
};
|
|
1328
1942
|
}
|
|
1329
|
-
async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previousState, scopedState, skillsDir, deps) {
|
|
1943
|
+
async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previousState, scopedState, skillsDir, deps, failures, linkOptions) {
|
|
1330
1944
|
if (isEmptySyncState(scopedState)) {
|
|
1331
1945
|
return 0;
|
|
1332
1946
|
}
|
|
1333
1947
|
const deletions = await deps.detectDeletedAgentSymlinks(
|
|
1334
|
-
pullResponse.skills
|
|
1948
|
+
pullResponse.skills.map((skill) => {
|
|
1949
|
+
const previous = isNativeSkill(skill) ? Object.entries(previousState.skills).find(([, row]) => row.cloudId === skill.id) : void 0;
|
|
1950
|
+
return previous ? { ...skill, name: previous[0] } : skill;
|
|
1951
|
+
}),
|
|
1335
1952
|
previousState,
|
|
1336
|
-
skillsDir
|
|
1953
|
+
skillsDir,
|
|
1954
|
+
linkOptions
|
|
1337
1955
|
);
|
|
1338
1956
|
if (deletions.length === 0) {
|
|
1339
1957
|
return 0;
|
|
1340
1958
|
}
|
|
1341
|
-
const latestSkillsById = new Map(pullResponse.skills.map((skill) => [skill.id, skill]));
|
|
1342
|
-
let fresh = null;
|
|
1343
|
-
try {
|
|
1344
|
-
fresh = await deps.pullSkills(serverUrl, jwt);
|
|
1345
|
-
} catch (error) {
|
|
1346
|
-
console.warn(
|
|
1347
|
-
"[skill-sync] Could not re-pull latest agent targets before deactivation; using the top-of-sync snapshot.",
|
|
1348
|
-
error
|
|
1349
|
-
);
|
|
1350
|
-
}
|
|
1351
|
-
if (fresh) {
|
|
1352
|
-
assertSkillsPullAuthorized(fresh);
|
|
1353
|
-
for (const skill of fresh.skills) {
|
|
1354
|
-
latestSkillsById.set(skill.id, skill);
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
1959
|
const agentsBySkill = /* @__PURE__ */ new Map();
|
|
1358
1960
|
for (const deletion of deletions) {
|
|
1359
1961
|
const entry = agentsBySkill.get(deletion.skillId) ?? {
|
|
@@ -1363,37 +1965,57 @@ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previo
|
|
|
1363
1965
|
entry.agents.add(deletion.agent);
|
|
1364
1966
|
agentsBySkill.set(deletion.skillId, entry);
|
|
1365
1967
|
}
|
|
1366
|
-
const
|
|
1968
|
+
const fresh = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
1969
|
+
assertSkillsPullAuthorized(fresh);
|
|
1970
|
+
assertSyncEnvironment(fresh, pullResponse);
|
|
1971
|
+
Object.assign(pullResponse, fresh);
|
|
1972
|
+
let needsRefresh = false;
|
|
1367
1973
|
let deactivated = 0;
|
|
1368
1974
|
for (const [skillId, { skillName, agents }] of agentsBySkill) {
|
|
1369
|
-
const
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1975
|
+
const skill = pullResponse.skills.find((item) => item.id === skillId);
|
|
1976
|
+
const previous = previousState.skills[skillName];
|
|
1977
|
+
if (!skill) continue;
|
|
1978
|
+
const native = isNativeSkill(skill);
|
|
1979
|
+
if (native) {
|
|
1980
|
+
if (!skill.native_settings || previous?.nativeSettingsRevision !== skill.native_settings.revision) continue;
|
|
1981
|
+
} else if (!skill.updated_at || previous?.cloudUpdatedAt !== skill.updated_at) continue;
|
|
1982
|
+
const patch = Object.fromEntries([...agents].map((agent) => [agent, false]));
|
|
1377
1983
|
try {
|
|
1378
|
-
await deps.updateAgentTargets(
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1984
|
+
const saved = await deps.updateAgentTargets(
|
|
1985
|
+
serverUrl,
|
|
1986
|
+
jwt,
|
|
1987
|
+
skillId,
|
|
1988
|
+
patch,
|
|
1989
|
+
native ? void 0 : skill.updated_at,
|
|
1990
|
+
native ? { reference: skill.native_reference, settingsRevision: skill.native_settings.revision } : void 0
|
|
1991
|
+
);
|
|
1992
|
+
if (saved.success !== true || !saved.updated_at?.trim() || !native && saved.updated_at === skill.updated_at || !["notis", "claude_code", "cursor", "codex"].every((agent) => typeof saved.agent_targets?.[agent] === "boolean") || ![...agents].every((agent) => saved.agent_targets[agent] === false)) {
|
|
1993
|
+
throw new Error("Assignment update did not return a verified saved revision");
|
|
1382
1994
|
}
|
|
1995
|
+
skill.agent_targets = saved.agent_targets;
|
|
1996
|
+
skill.updated_at = saved.updated_at;
|
|
1997
|
+
if (native) needsRefresh = true;
|
|
1383
1998
|
deactivated += agents.size;
|
|
1384
1999
|
} catch (error) {
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
);
|
|
2000
|
+
needsRefresh = true;
|
|
2001
|
+
failures.push({ name: skillName, error: "Could not save the local agent removal; refreshed saved assignments" });
|
|
2002
|
+
console.warn(`[skill-sync] Assignment changed or could not be saved for "${skillName}"; refreshing before reconciliation.`, error);
|
|
1389
2003
|
}
|
|
1390
2004
|
}
|
|
2005
|
+
if (needsRefresh) {
|
|
2006
|
+
const refreshed = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
2007
|
+
assertSkillsPullAuthorized(refreshed);
|
|
2008
|
+
assertSyncEnvironment(refreshed, pullResponse);
|
|
2009
|
+
Object.assign(pullResponse, refreshed);
|
|
2010
|
+
}
|
|
1391
2011
|
return deactivated;
|
|
1392
2012
|
}
|
|
1393
2013
|
async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
2014
|
+
const linkAgentFolders = options.linkAgentFolders !== false;
|
|
1394
2015
|
const deps = {
|
|
1395
2016
|
...DEFAULT_RUN_SKILL_SYNC_DEPS,
|
|
1396
|
-
...dependencies
|
|
2017
|
+
...dependencies,
|
|
2018
|
+
...linkAgentFolders ? {} : MIRROR_ONLY_DEPS
|
|
1397
2019
|
};
|
|
1398
2020
|
const syncSettings = await deps.fetchSyncSettings(
|
|
1399
2021
|
serverUrl,
|
|
@@ -1405,11 +2027,12 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
1405
2027
|
"Cannot sync skills without a server-verified account identity."
|
|
1406
2028
|
);
|
|
1407
2029
|
}
|
|
1408
|
-
const syncPaths = getSkillSyncPathsForUser(syncUserId);
|
|
1409
|
-
const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
|
|
1410
|
-
syncPaths.skillsDir
|
|
1411
|
-
);
|
|
2030
|
+
const syncPaths = getSkillSyncPathsForUser(syncUserId, void 0, syncSettings.sync_namespace, options.syncHome);
|
|
1412
2031
|
if (options.honorSyncEnabled !== false && !syncSettings.sync_enabled) {
|
|
2032
|
+
const foreignLinksRemoved2 = await deps.removeForeignAccountSymlinks(
|
|
2033
|
+
syncPaths.skillsDir,
|
|
2034
|
+
isolatedLinkOptions(syncPaths)
|
|
2035
|
+
);
|
|
1413
2036
|
return {
|
|
1414
2037
|
syncEnabled: false,
|
|
1415
2038
|
pushed: 0,
|
|
@@ -1418,7 +2041,7 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
1418
2041
|
deleted: 0,
|
|
1419
2042
|
deactivated: 0,
|
|
1420
2043
|
linked: 0,
|
|
1421
|
-
removed:
|
|
2044
|
+
removed: foreignLinksRemoved2,
|
|
1422
2045
|
skipped: 0,
|
|
1423
2046
|
lastSyncedAt: syncSettings.last_synced_at,
|
|
1424
2047
|
failedPushes: []
|
|
@@ -1426,52 +2049,77 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
1426
2049
|
}
|
|
1427
2050
|
let pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
1428
2051
|
assertSkillsPullAuthorized(pullResponse);
|
|
2052
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
2053
|
+
const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
|
|
2054
|
+
syncPaths.skillsDir,
|
|
2055
|
+
isolatedLinkOptions(syncPaths)
|
|
2056
|
+
);
|
|
2057
|
+
if (linkAgentFolders && !syncPaths.isolatedNamespace) {
|
|
2058
|
+
await forgetOtherAccountsAgentLinks(syncPaths);
|
|
2059
|
+
}
|
|
1429
2060
|
const cloudCuratedSkillNames = new Set(
|
|
1430
2061
|
pullResponse.skills.filter((skill) => skill.source === "curated").map((skill) => skill.name)
|
|
1431
2062
|
);
|
|
1432
2063
|
const protectedSkillNames = /* @__PURE__ */ new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
|
|
2064
|
+
const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
|
|
2065
|
+
const assignmentFailures = [];
|
|
2066
|
+
const deactivated = linkAgentFolders && !syncPaths.isolatedNamespace && syncSettings.agent_targets_conditional_updates === true ? await deactivateDeletedAgentSkills(
|
|
2067
|
+
serverUrl,
|
|
2068
|
+
jwt,
|
|
2069
|
+
pullResponse,
|
|
2070
|
+
scopedState,
|
|
2071
|
+
scopedState,
|
|
2072
|
+
syncPaths.skillsDir,
|
|
2073
|
+
deps,
|
|
2074
|
+
assignmentFailures,
|
|
2075
|
+
isolatedLinkOptions(syncPaths)
|
|
2076
|
+
) : 0;
|
|
2077
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
1433
2078
|
const authUserId = decodeJwtSubject(jwt);
|
|
1434
2079
|
let previousAuthState = null;
|
|
1435
|
-
if (authUserId && authUserId !== syncUserId) {
|
|
1436
|
-
const previousAuthPaths = getSkillSyncPathsForUser(authUserId);
|
|
2080
|
+
if (!syncPaths.isolatedNamespace && authUserId && authUserId !== syncUserId) {
|
|
2081
|
+
const previousAuthPaths = getSkillSyncPathsForUser(authUserId, void 0, void 0, options.syncHome);
|
|
1437
2082
|
previousAuthState = await deps.readSyncState(previousAuthPaths);
|
|
1438
2083
|
await deps.gatherTopLevelLocalSkills(syncPaths, {
|
|
1439
2084
|
sourceRoots: [{ label: "previous-auth-scope", root: previousAuthPaths.skillsDir }],
|
|
1440
2085
|
protectedSkillNames
|
|
1441
2086
|
});
|
|
1442
2087
|
}
|
|
1443
|
-
|
|
1444
|
-
protectedSkillNames
|
|
1445
|
-
}
|
|
2088
|
+
if (linkAgentFolders && !syncPaths.isolatedNamespace) {
|
|
2089
|
+
await deps.gatherTopLevelLocalSkills(syncPaths, { protectedSkillNames });
|
|
2090
|
+
}
|
|
1446
2091
|
const localSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
|
|
1447
|
-
const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
|
|
1448
2092
|
const previousState = withoutBaseSkillState(applyLegacyFirstRunState(
|
|
1449
2093
|
localSkills,
|
|
1450
2094
|
scopedState,
|
|
1451
|
-
isEmptySyncState(scopedState) ? !previousAuthState || isEmptySyncState(previousAuthState) ? await deps.readLegacySyncState(syncPaths) : previousAuthState : null
|
|
2095
|
+
!syncPaths.isolatedNamespace && isEmptySyncState(scopedState) ? !previousAuthState || isEmptySyncState(previousAuthState) ? await deps.readLegacySyncState(syncPaths) : previousAuthState : null
|
|
1452
2096
|
));
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
jwt,
|
|
1456
|
-
pullResponse,
|
|
1457
|
-
previousState,
|
|
1458
|
-
scopedState,
|
|
1459
|
-
syncPaths.skillsDir,
|
|
1460
|
-
deps
|
|
1461
|
-
);
|
|
1462
|
-
await deps.syncSymlinks(
|
|
2097
|
+
pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
|
|
2098
|
+
const gatheredSymlinkResult = await deps.syncSymlinks(
|
|
1463
2099
|
buildLocalSymlinkCandidates(pullResponse, localSkills, previousState),
|
|
1464
|
-
syncPaths.skillsDir
|
|
2100
|
+
syncPaths.skillsDir,
|
|
2101
|
+
isolatedLinkOptions(syncPaths)
|
|
1465
2102
|
);
|
|
1466
2103
|
const pushCandidates = getPushCandidates(
|
|
1467
2104
|
localSkills,
|
|
1468
2105
|
previousState,
|
|
1469
2106
|
cloudCuratedSkillNames,
|
|
1470
|
-
new Set(pullResponse.skills.map((skill) => skill.name))
|
|
2107
|
+
new Set(pullResponse.skills.map((skill) => skill.name)),
|
|
2108
|
+
pullResponse.skills
|
|
1471
2109
|
);
|
|
1472
2110
|
const failedPushes = [];
|
|
2111
|
+
const acknowledgedNative = /* @__PURE__ */ new Map();
|
|
2112
|
+
let writeLocalSkills = localSkills;
|
|
1473
2113
|
if (pushCandidates.length > 0) {
|
|
1474
2114
|
const pushResult = await deps.pushChangedSkills(serverUrl, jwt, pushCandidates);
|
|
2115
|
+
for (const result of pushResult.skills || []) {
|
|
2116
|
+
const saved = result;
|
|
2117
|
+
const candidate = pushCandidates.find((local) => local.nativeReference && local.cloudId === saved.id);
|
|
2118
|
+
if (candidate && saved.native_reference) {
|
|
2119
|
+
checkedNativeReference(saved.native_reference, candidate.cloudId);
|
|
2120
|
+
acknowledgedNative.set(candidate.cloudId, candidate.folderHash);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
1475
2123
|
if (Array.isArray(pushResult?.failed) && pushResult.failed.length > 0) {
|
|
1476
2124
|
failedPushes.push(...pushResult.failed);
|
|
1477
2125
|
console.warn(
|
|
@@ -1480,31 +2128,58 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
1480
2128
|
}
|
|
1481
2129
|
pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
1482
2130
|
assertSkillsPullAuthorized(pullResponse);
|
|
2131
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
2132
|
+
writeLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
|
|
2133
|
+
pullResponse = bindNativeFolders(pullResponse, writeLocalSkills, previousState);
|
|
1483
2134
|
}
|
|
1484
2135
|
const cloudSkillNames = new Set(pullResponse.skills.map((s) => s.name));
|
|
1485
2136
|
let deleted = 0;
|
|
1486
2137
|
for (const skillName of Object.keys(previousState.skills)) {
|
|
1487
2138
|
if (!cloudSkillNames.has(skillName)) {
|
|
2139
|
+
const previous = previousState.skills[skillName];
|
|
2140
|
+
if (previous.nativeReference || previous.nativeUnavailable || pullResponse.skills.some((skill) => isNativeSkill(skill) && skill.id === previous.cloudId)) {
|
|
2141
|
+
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
1488
2144
|
await deps.deleteLocalSkill(skillName, syncPaths);
|
|
1489
|
-
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir);
|
|
2145
|
+
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
1490
2146
|
deleted += 1;
|
|
1491
2147
|
}
|
|
1492
2148
|
}
|
|
2149
|
+
const failedDownloads = [];
|
|
2150
|
+
const writtenSkillNames = /* @__PURE__ */ new Set();
|
|
2151
|
+
const appliedFilesByName = {};
|
|
1493
2152
|
const downloaded = await writePulledSkillsToScopedMirror(
|
|
1494
2153
|
pullResponse,
|
|
1495
|
-
|
|
2154
|
+
writeLocalSkills,
|
|
1496
2155
|
previousState,
|
|
1497
2156
|
syncPaths,
|
|
1498
|
-
deps
|
|
2157
|
+
deps,
|
|
2158
|
+
failedDownloads,
|
|
2159
|
+
writtenSkillNames,
|
|
2160
|
+
appliedFilesByName,
|
|
2161
|
+
acknowledgedNative
|
|
1499
2162
|
);
|
|
1500
2163
|
const finalLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
|
|
1501
2164
|
const symlinkResult = await deps.syncSymlinks(
|
|
1502
2165
|
buildLocalSymlinkCandidates(pullResponse, finalLocalSkills, previousState),
|
|
1503
|
-
syncPaths.skillsDir
|
|
2166
|
+
syncPaths.skillsDir,
|
|
2167
|
+
isolatedLinkOptions(syncPaths)
|
|
1504
2168
|
);
|
|
2169
|
+
const verifiedLinks = { ...symlinkResult.verifiedAgentLinks ?? {} };
|
|
2170
|
+
for (const failure of failedDownloads) delete verifiedLinks[failure.name];
|
|
1505
2171
|
const lastSyncedAt = pullResponse.last_synced_at || (/* @__PURE__ */ new Date()).toISOString();
|
|
1506
2172
|
await deps.writeSyncState(
|
|
1507
|
-
buildSyncState(
|
|
2173
|
+
buildSyncState(
|
|
2174
|
+
pullResponse,
|
|
2175
|
+
finalLocalSkills,
|
|
2176
|
+
lastSyncedAt,
|
|
2177
|
+
verifiedLinks,
|
|
2178
|
+
new Set(failedDownloads.map((item) => item.name)),
|
|
2179
|
+
collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
|
|
2180
|
+
collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
|
|
2181
|
+
previousState
|
|
2182
|
+
),
|
|
1508
2183
|
syncPaths
|
|
1509
2184
|
);
|
|
1510
2185
|
return {
|
|
@@ -1514,15 +2189,21 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
1514
2189
|
downloaded,
|
|
1515
2190
|
deleted,
|
|
1516
2191
|
deactivated,
|
|
1517
|
-
linked: symlinkResult.linked,
|
|
1518
|
-
removed: foreignLinksRemoved + symlinkResult.removed,
|
|
2192
|
+
linked: gatheredSymlinkResult.linked + symlinkResult.linked,
|
|
2193
|
+
removed: foreignLinksRemoved + gatheredSymlinkResult.removed + symlinkResult.removed,
|
|
1519
2194
|
skipped: symlinkResult.skipped,
|
|
2195
|
+
failedLinks: [...assignmentFailures, ...failedDownloads, ...(symlinkResult.failures ?? []).filter(
|
|
2196
|
+
(failure) => !failedDownloads.some((download) => download.name === failure.name)
|
|
2197
|
+
)],
|
|
1520
2198
|
lastSyncedAt,
|
|
1521
2199
|
failedPushes
|
|
1522
2200
|
};
|
|
1523
2201
|
}
|
|
1524
2202
|
export {
|
|
2203
|
+
fetchSyncSettings,
|
|
2204
|
+
isolatedLinkOptions,
|
|
1525
2205
|
materializeCloudSkillsForLocalShell,
|
|
1526
|
-
runSkillSync
|
|
2206
|
+
runSkillSync,
|
|
2207
|
+
shouldWriteCloudSkill
|
|
1527
2208
|
};
|
|
1528
2209
|
//# sourceMappingURL=index.js.map
|