@notis_ai/cli 0.2.18 → 0.2.20
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 +42325 -18273
- 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 +279 -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 -1570
- 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 -3011
- 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
|
@@ -6,10 +6,13 @@ import os from "os";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { promisify } from "util";
|
|
8
8
|
|
|
9
|
-
import type { CloudSkill, LocalSkill, NotisSyncState } from "./types";
|
|
9
|
+
import type { BundleFile, CloudSkill, LocalSkill, NotisSyncState, SkillWriteOutcome } from "./types";
|
|
10
|
+
import { NATIVE_IDENTITY_FILE, assertNativePull, checkedNativeIdentity, isNativeSkill,
|
|
11
|
+
nativeFileDigest, nativeFrontmatterName, type NativeSkillIdentity } from './native-identity';
|
|
10
12
|
|
|
11
13
|
const HOME_DIR = os.homedir();
|
|
12
14
|
const execFileAsync = promisify(execFile);
|
|
15
|
+
class NativeSkillIdentityError extends Error {}
|
|
13
16
|
|
|
14
17
|
export const AGENTS_DIR = path.join(HOME_DIR, ".agents");
|
|
15
18
|
export const LEGACY_AGENTS_SKILLS_DIR = path.join(AGENTS_DIR, "skills");
|
|
@@ -45,6 +48,9 @@ const EXCLUDED_TOP_LEVEL_ROOT_NAMES = new Set([
|
|
|
45
48
|
]);
|
|
46
49
|
|
|
47
50
|
export interface SkillSyncPaths {
|
|
51
|
+
/** Explicit process-independent home for a confined sync invocation. */
|
|
52
|
+
homeDir?: string;
|
|
53
|
+
isolatedNamespace?: string;
|
|
48
54
|
agentsDir: string;
|
|
49
55
|
syncRoot: string;
|
|
50
56
|
legacySkillsDir: string;
|
|
@@ -96,14 +102,21 @@ function sanitizePathSegment(value: string): string {
|
|
|
96
102
|
export function getSkillSyncPathsForUser(
|
|
97
103
|
authUserId: string,
|
|
98
104
|
agentsDir: string = AGENTS_DIR,
|
|
105
|
+
namespace?: string | null,
|
|
106
|
+
syncHome?: string,
|
|
99
107
|
): SkillSyncPaths {
|
|
100
108
|
const safeUserId = sanitizePathSegment(authUserId);
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
109
|
+
const homeDir = syncHome ? path.resolve(syncHome) : undefined;
|
|
110
|
+
const resolvedAgentsDir = homeDir ? path.join(homeDir, '.agents') : path.resolve(agentsDir);
|
|
111
|
+
const syncRoot = homeDir ? path.join(homeDir, '.notis', 'skills') : getDefaultSyncRootForAgentsDir(resolvedAgentsDir);
|
|
112
|
+
const userRoot = namespace
|
|
113
|
+
? path.join(syncRoot, 'environments', sanitizePathSegment(namespace), 'users', safeUserId)
|
|
114
|
+
: path.join(syncRoot, "users", safeUserId);
|
|
104
115
|
const legacyUserRoot = path.join(resolvedAgentsDir, "notis", "users", safeUserId);
|
|
105
116
|
|
|
106
117
|
return {
|
|
118
|
+
...(homeDir ? { homeDir } : {}),
|
|
119
|
+
...(namespace ? { isolatedNamespace: sanitizePathSegment(namespace) } : {}),
|
|
107
120
|
agentsDir: resolvedAgentsDir,
|
|
108
121
|
syncRoot,
|
|
109
122
|
legacySkillsDir: path.join(resolvedAgentsDir, "skills"),
|
|
@@ -163,17 +176,29 @@ async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
|
163
176
|
}
|
|
164
177
|
}
|
|
165
178
|
|
|
166
|
-
|
|
179
|
+
// Interpreter caches a script run leaves beside itself are not skill content: they
|
|
180
|
+
// must neither mark a skill as changed nor ship in its bundle (app bundling applies
|
|
181
|
+
// the same rule).
|
|
182
|
+
const IGNORED_SKILL_DIRECTORIES = new Set(["__pycache__", ".pytest_cache"]);
|
|
183
|
+
|
|
184
|
+
function isIgnoredSkillEntry(entry: Dirent): boolean {
|
|
185
|
+
if (entry.name === ".DS_Store") return true;
|
|
186
|
+
if (entry.isDirectory()) return IGNORED_SKILL_DIRECTORIES.has(entry.name);
|
|
187
|
+
return /\.py[co]$/i.test(entry.name);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function listFilesRecursive(dirPath: string, rootPath = dirPath, strictLinks = false): Promise<string[]> {
|
|
167
191
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
168
192
|
const nested = await Promise.all(
|
|
169
193
|
entries.map(async (entry) => {
|
|
170
|
-
if (entry.name ===
|
|
194
|
+
if (isIgnoredSkillEntry(entry) || (dirPath === rootPath && entry.name === NATIVE_IDENTITY_FILE)) {
|
|
171
195
|
return [];
|
|
172
196
|
}
|
|
173
197
|
|
|
174
198
|
const fullPath = path.join(dirPath, entry.name);
|
|
199
|
+
if (strictLinks && entry.isSymbolicLink()) throw new Error('Native Skill bundles cannot contain symbolic links.');
|
|
175
200
|
if (entry.isDirectory()) {
|
|
176
|
-
return listFilesRecursive(fullPath);
|
|
201
|
+
return listFilesRecursive(fullPath, rootPath, strictLinks);
|
|
177
202
|
}
|
|
178
203
|
|
|
179
204
|
if (entry.isFile()) {
|
|
@@ -326,17 +351,17 @@ function defaultTopLevelSkillSources(
|
|
|
326
351
|
{ label: "agents", root: paths.legacySkillsDir, priority: 2 },
|
|
327
352
|
{
|
|
328
353
|
label: "codex",
|
|
329
|
-
root: path.join(HOME_DIR, ".codex", "skills"),
|
|
354
|
+
root: path.join(paths.homeDir || HOME_DIR, ".codex", "skills"),
|
|
330
355
|
priority: 3,
|
|
331
356
|
},
|
|
332
357
|
{
|
|
333
358
|
label: "cursor",
|
|
334
|
-
root: path.join(HOME_DIR, ".cursor", "skills"),
|
|
359
|
+
root: path.join(paths.homeDir || HOME_DIR, ".cursor", "skills"),
|
|
335
360
|
priority: 4,
|
|
336
361
|
},
|
|
337
362
|
{
|
|
338
363
|
label: "claude",
|
|
339
|
-
root: path.join(HOME_DIR, ".claude", "skills"),
|
|
364
|
+
root: path.join(paths.homeDir || HOME_DIR, ".claude", "skills"),
|
|
340
365
|
priority: 5,
|
|
341
366
|
},
|
|
342
367
|
);
|
|
@@ -680,6 +705,7 @@ export async function scanLocalSkills(
|
|
|
680
705
|
await ensureCanonicalSkillsDir(paths);
|
|
681
706
|
|
|
682
707
|
const sourceUrls = await readSkillLock(paths);
|
|
708
|
+
const previousState = await readSyncState(paths);
|
|
683
709
|
const entries = await fs.readdir(paths.skillsDir, { withFileTypes: true });
|
|
684
710
|
const skills: Array<LocalSkill | null> = await Promise.all(
|
|
685
711
|
entries.map(async (entry) => {
|
|
@@ -692,7 +718,17 @@ export async function scanLocalSkills(
|
|
|
692
718
|
|
|
693
719
|
const skillDir = path.join(paths.skillsDir, entry.name);
|
|
694
720
|
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
695
|
-
|
|
721
|
+
let marker: string | null = null;
|
|
722
|
+
// Classify provenance BEFORE reading potentially unavailable content.
|
|
723
|
+
// Failure to scan a managed folder is not evidence that it is absent.
|
|
724
|
+
try { marker = await fs.readFile(path.join(skillDir, NATIVE_IDENTITY_FILE), 'utf8'); }
|
|
725
|
+
catch (error) {
|
|
726
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
727
|
+
throw new NativeSkillIdentityError('Native Skill provenance could not be read; existing files were retained.');
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const knownNative = marker !== null || Boolean(previousState.skills[entry.name]?.nativeReference)
|
|
731
|
+
|| Object.values(previousState.nativeHistory || {}).some(row => row.names.includes(entry.name));
|
|
696
732
|
try {
|
|
697
733
|
const skillMd = await fs.readFile(skillMdPath, "utf8");
|
|
698
734
|
const { description } = parseFrontMatter(skillMd);
|
|
@@ -704,11 +740,27 @@ export async function scanLocalSkills(
|
|
|
704
740
|
folderHash,
|
|
705
741
|
directoryPath: skillDir,
|
|
706
742
|
};
|
|
743
|
+
if (marker !== null) {
|
|
744
|
+
let identity: NativeSkillIdentity;
|
|
745
|
+
try { identity = checkedNativeIdentity(JSON.parse(marker)); }
|
|
746
|
+
catch { throw new NativeSkillIdentityError('Native Skill provenance is unreadable; do not upload it as a new Skill.'); }
|
|
747
|
+
let current = true;
|
|
748
|
+
for (const [name, expected] of Object.entries(identity.files)) {
|
|
749
|
+
try { if (nativeFileDigest(await fs.readFile(resolveSkillBundleFilePath(skillDir, name))) !== expected) current = false; }
|
|
750
|
+
catch { current = false; }
|
|
751
|
+
}
|
|
752
|
+
const editedName = nativeFrontmatterName(skillMd);
|
|
753
|
+
Object.assign(skill, { cloudId: identity.cloudId, nativeReference: identity.reference,
|
|
754
|
+
nativeScope: identity.scope, nativeBaseFolderHash: identity.folderHash, nativeFilesCurrent: current,
|
|
755
|
+
displayName: editedName && editedName !== identity.frontmatterName ? editedName : identity.displayName });
|
|
756
|
+
}
|
|
707
757
|
if (sourceUrls[entry.name]) {
|
|
708
758
|
skill.sourceUrl = sourceUrls[entry.name];
|
|
709
759
|
}
|
|
710
760
|
return skill;
|
|
711
|
-
} catch {
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (error instanceof NativeSkillIdentityError) throw error;
|
|
763
|
+
if (knownNative) throw new NativeSkillIdentityError('Native Skill files could not be read; existing files were retained.');
|
|
712
764
|
return null;
|
|
713
765
|
}
|
|
714
766
|
}),
|
|
@@ -725,6 +777,11 @@ function normalizeSyncState(
|
|
|
725
777
|
if (!state || state.version !== 1 || typeof state.skills !== "object") {
|
|
726
778
|
return null;
|
|
727
779
|
}
|
|
780
|
+
if (state.nativeHistory && (typeof state.nativeHistory !== 'object' || Array.isArray(state.nativeHistory)
|
|
781
|
+
|| Object.values(state.nativeHistory).some(row => !row || !Array.isArray(row.names)
|
|
782
|
+
|| row.names.some(name => typeof name !== 'string')))) {
|
|
783
|
+
throw new Error('Native Skill sync provenance is unreadable; existing files were retained.');
|
|
784
|
+
}
|
|
728
785
|
const normalizeStoredAgentTargets = (
|
|
729
786
|
targets: Partial<NotisSyncState["skills"][string]["agentTargets"]> | null | undefined,
|
|
730
787
|
): NotisSyncState["skills"][string]["agentTargets"] => ({
|
|
@@ -736,6 +793,7 @@ function normalizeSyncState(
|
|
|
736
793
|
|
|
737
794
|
return {
|
|
738
795
|
version: 1,
|
|
796
|
+
...(state.nativeHistory ? { nativeHistory: state.nativeHistory } : {}),
|
|
739
797
|
lastSyncedAt:
|
|
740
798
|
typeof state.lastSyncedAt === "string" ? state.lastSyncedAt : null,
|
|
741
799
|
skills: Object.fromEntries(
|
|
@@ -839,28 +897,65 @@ async function createZipFromDirectory(directoryPath: string): Promise<string> {
|
|
|
839
897
|
os.tmpdir(),
|
|
840
898
|
`notis-skill-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`,
|
|
841
899
|
);
|
|
842
|
-
const parentDir = path.dirname(directoryPath);
|
|
843
900
|
const directoryName = path.basename(directoryPath);
|
|
901
|
+
// Zip a copy holding exactly the files the folder hash covers, so ignored
|
|
902
|
+
// caches never ship.
|
|
903
|
+
const stagingRoot = await fs.mkdtemp(path.join(os.tmpdir(), "notis-skill-bundle-"));
|
|
904
|
+
try {
|
|
905
|
+
const stagedDir = path.join(stagingRoot, directoryName);
|
|
906
|
+
for (const filePath of await listFilesRecursive(directoryPath)) {
|
|
907
|
+
const target = path.join(stagedDir, path.relative(directoryPath, filePath));
|
|
908
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
909
|
+
await fs.copyFile(filePath, target);
|
|
910
|
+
}
|
|
911
|
+
await fs.mkdir(stagedDir, { recursive: true });
|
|
912
|
+
|
|
913
|
+
if (process.platform === "win32") {
|
|
914
|
+
// Windows PowerShell's Compress-Archive writes entry names with
|
|
915
|
+
// backslashes, which unzip on the cloud computer as literal file names
|
|
916
|
+
// ("cached\\SKILL.md"). bsdtar, shipped in System32 since Windows 10
|
|
917
|
+
// 1803, writes the forward slashes the zip format requires. Use the
|
|
918
|
+
// system copy explicitly: on a Git Bash PATH, "tar" resolves to GNU tar,
|
|
919
|
+
// which cannot write zip archives.
|
|
920
|
+
const systemTar = path.join(
|
|
921
|
+
process.env.SystemRoot || "C:\\Windows",
|
|
922
|
+
"System32",
|
|
923
|
+
"tar.exe",
|
|
924
|
+
);
|
|
925
|
+
if (await pathExists(systemTar)) {
|
|
926
|
+
await execFileAsync(systemTar, [
|
|
927
|
+
"-c",
|
|
928
|
+
"--format",
|
|
929
|
+
"zip",
|
|
930
|
+
"-f",
|
|
931
|
+
zipPath,
|
|
932
|
+
"-C",
|
|
933
|
+
stagingRoot,
|
|
934
|
+
directoryName,
|
|
935
|
+
]);
|
|
936
|
+
return zipPath;
|
|
937
|
+
}
|
|
938
|
+
await execFileAsync("powershell.exe", [
|
|
939
|
+
"-NoProfile",
|
|
940
|
+
"-Command",
|
|
941
|
+
`Compress-Archive -Path '${stagedDir.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`,
|
|
942
|
+
]);
|
|
943
|
+
return zipPath;
|
|
944
|
+
}
|
|
844
945
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
"-Command",
|
|
849
|
-
`Compress-Archive -Path '${directoryPath.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`,
|
|
850
|
-
]);
|
|
946
|
+
await execFileAsync("zip", ["-qry", zipPath, directoryName], {
|
|
947
|
+
cwd: stagingRoot,
|
|
948
|
+
});
|
|
851
949
|
return zipPath;
|
|
950
|
+
} finally {
|
|
951
|
+
await fs.rm(stagingRoot, { recursive: true, force: true });
|
|
852
952
|
}
|
|
853
|
-
|
|
854
|
-
await execFileAsync("zip", ["-qry", zipPath, directoryName], {
|
|
855
|
-
cwd: parentDir,
|
|
856
|
-
});
|
|
857
|
-
return zipPath;
|
|
858
953
|
}
|
|
859
954
|
|
|
860
955
|
async function extractZipToDirectory(
|
|
861
956
|
zipPath: string,
|
|
862
957
|
destinationDir: string,
|
|
863
|
-
): Promise<
|
|
958
|
+
): Promise<string[]> {
|
|
864
959
|
const extractRoot = await fs.mkdtemp(
|
|
865
960
|
path.join(os.tmpdir(), "notis-skill-extract-"),
|
|
866
961
|
);
|
|
@@ -889,11 +984,62 @@ async function extractZipToDirectory(
|
|
|
889
984
|
await fs.rm(destinationDir, { recursive: true, force: true });
|
|
890
985
|
await fs.mkdir(path.dirname(destinationDir), { recursive: true });
|
|
891
986
|
await fs.cp(sourceDir, destinationDir, { recursive: true });
|
|
987
|
+
return await listRelativeFiles(destinationDir);
|
|
892
988
|
} finally {
|
|
893
989
|
await fs.rm(extractRoot, { recursive: true, force: true });
|
|
894
990
|
}
|
|
895
991
|
}
|
|
896
992
|
|
|
993
|
+
async function listRelativeFiles(dirPath: string): Promise<string[]> {
|
|
994
|
+
try {
|
|
995
|
+
const files = await listFilesRecursive(dirPath);
|
|
996
|
+
return files.map((filePath) => toPosixRelativePath(dirPath, filePath)).sort();
|
|
997
|
+
} catch (error) {
|
|
998
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return [];
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function toPosixRelativePath(rootDir: string, filePath: string): string {
|
|
1004
|
+
return path.relative(rootDir, filePath).split(path.sep).join("/");
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Carry local-only files across a cloud write. A skill that writes while it runs
|
|
1009
|
+
* (a run ledger under its own folder) would otherwise lose that work every time the
|
|
1010
|
+
* cloud content changed, because the write replaces the whole directory.
|
|
1011
|
+
*
|
|
1012
|
+
* Selective, not a blanket merge:
|
|
1013
|
+
* - a path the incoming content provides always wins;
|
|
1014
|
+
* - a path the previous write applied and the cloud has now dropped stays deleted,
|
|
1015
|
+
* so upstream removals still propagate;
|
|
1016
|
+
* - anything else is local and is kept.
|
|
1017
|
+
*
|
|
1018
|
+
* With no record of the previous write (state written before this existed) nothing
|
|
1019
|
+
* can be attributed to the cloud, so local files are kept rather than destroyed.
|
|
1020
|
+
*/
|
|
1021
|
+
async function preserveUnmanagedFiles(
|
|
1022
|
+
existingDir: string,
|
|
1023
|
+
stagingDir: string,
|
|
1024
|
+
previouslyApplied?: readonly string[],
|
|
1025
|
+
strictConflicts = false,
|
|
1026
|
+
): Promise<void> {
|
|
1027
|
+
const managed = previouslyApplied ? new Set(previouslyApplied) : null;
|
|
1028
|
+
for (const existingFile of await listFilesRecursive(existingDir)) {
|
|
1029
|
+
const relativePath = toPosixRelativePath(existingDir, existingFile);
|
|
1030
|
+
if (managed?.has(relativePath)) continue;
|
|
1031
|
+
const stagedPath = path.join(stagingDir, ...relativePath.split("/"));
|
|
1032
|
+
if (await pathExists(stagedPath)) {
|
|
1033
|
+
if (strictConflicts && !(await fs.readFile(existingFile)).equals(await fs.readFile(stagedPath))) {
|
|
1034
|
+
throw new Error('An incoming native Skill file conflicts with a local file. Local files were retained.');
|
|
1035
|
+
}
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
await fs.mkdir(path.dirname(stagedPath), { recursive: true });
|
|
1039
|
+
await fs.copyFile(existingFile, stagedPath);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
897
1043
|
async function pathExists(targetPath: string): Promise<boolean> {
|
|
898
1044
|
try {
|
|
899
1045
|
await fs.access(targetPath);
|
|
@@ -906,6 +1052,7 @@ async function pathExists(targetPath: string): Promise<boolean> {
|
|
|
906
1052
|
async function replaceSkillDirectoryAtomically(
|
|
907
1053
|
skillDir: string,
|
|
908
1054
|
populateDir: (stagingDir: string) => Promise<void>,
|
|
1055
|
+
preserve?: { previouslyApplied?: readonly string[]; expectedFolderHash?: string; native?: boolean; legacyBaseline?: boolean },
|
|
909
1056
|
): Promise<void> {
|
|
910
1057
|
const parentDir = path.dirname(skillDir);
|
|
911
1058
|
const skillName = path.basename(skillDir);
|
|
@@ -924,11 +1071,44 @@ async function replaceSkillDirectoryAtomically(
|
|
|
924
1071
|
let cleanupError: Error | null = null;
|
|
925
1072
|
|
|
926
1073
|
try {
|
|
1074
|
+
if (preserve?.native && preserve.expectedFolderHash === undefined) {
|
|
1075
|
+
let existing = false;
|
|
1076
|
+
try { await fs.lstat(skillDir); existing = true; }
|
|
1077
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
|
|
1078
|
+
if (existing) throw new Error('Existing native Skill files need a readable baseline before replacement.');
|
|
1079
|
+
}
|
|
1080
|
+
if (preserve?.expectedFolderHash !== undefined
|
|
1081
|
+
&& (!await pathExists(skillDir) || await computeFolderHash(skillDir) !== preserve.expectedFolderHash)) {
|
|
1082
|
+
throw new Error('Local Skill files changed before sync; the newer local edits were retained.');
|
|
1083
|
+
}
|
|
927
1084
|
await populateDir(stagingDir);
|
|
928
1085
|
|
|
1086
|
+
if (preserve && (await pathExists(skillDir))) {
|
|
1087
|
+
await preserveUnmanagedFiles(skillDir, stagingDir, preserve.previouslyApplied,
|
|
1088
|
+
preserve.native && !preserve.legacyBaseline);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (preserve?.native) {
|
|
1092
|
+
const markerPath = path.join(stagingDir, NATIVE_IDENTITY_FILE);
|
|
1093
|
+
const marker = checkedNativeIdentity(JSON.parse(await fs.readFile(markerPath, 'utf8')));
|
|
1094
|
+
marker.folderHash = await computeFolderHash(stagingDir);
|
|
1095
|
+
await fs.writeFile(markerPath, JSON.stringify(marker), { mode: 0o600 });
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (preserve?.expectedFolderHash !== undefined
|
|
1099
|
+
&& (!await pathExists(skillDir) || await computeFolderHash(skillDir) !== preserve.expectedFolderHash)) {
|
|
1100
|
+
throw new Error('Local Skill files changed during sync; the newer local edits were retained.');
|
|
1101
|
+
}
|
|
1102
|
+
|
|
929
1103
|
if (await pathExists(skillDir)) {
|
|
930
1104
|
await fs.rename(skillDir, backupDir);
|
|
931
1105
|
movedExisting = true;
|
|
1106
|
+
if (preserve?.native && preserve.expectedFolderHash === undefined) {
|
|
1107
|
+
throw new Error('Native Skill files appeared during sync; existing files were retained.');
|
|
1108
|
+
}
|
|
1109
|
+
if (preserve?.expectedFolderHash !== undefined && await computeFolderHash(backupDir) !== preserve.expectedFolderHash) {
|
|
1110
|
+
throw new Error('Local Skill files changed before publication; the newer local edits were retained.');
|
|
1111
|
+
}
|
|
932
1112
|
}
|
|
933
1113
|
|
|
934
1114
|
await fs.rename(stagingDir, skillDir);
|
|
@@ -982,15 +1162,38 @@ export async function createSkillBundleBase64(
|
|
|
982
1162
|
}
|
|
983
1163
|
}
|
|
984
1164
|
|
|
1165
|
+
export async function captureNativeSkillFiles(skill: LocalSkill): Promise<BundleFile[]> {
|
|
1166
|
+
const hash = createHash('sha256');
|
|
1167
|
+
const files: BundleFile[] = [];
|
|
1168
|
+
for (const filePath of await listFilesRecursive(skill.directoryPath, skill.directoryPath, true)) {
|
|
1169
|
+
const relative = path.relative(skill.directoryPath, filePath);
|
|
1170
|
+
const bytes = await fs.readFile(filePath);
|
|
1171
|
+
hash.update(relative); hash.update('\0'); hash.update(bytes); hash.update('\0');
|
|
1172
|
+
files.push({ path: relative.split(path.sep).join('/'), content_b64: bytes.toString('base64') });
|
|
1173
|
+
}
|
|
1174
|
+
if (hash.digest('hex') !== skill.folderHash) throw new Error('Local Skill files changed before upload. Sync again with the current files.');
|
|
1175
|
+
return files;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
985
1178
|
export async function writeCloudSkillToDisk(
|
|
986
1179
|
skill: CloudSkill,
|
|
987
1180
|
bundleBytes?: Buffer,
|
|
988
1181
|
paths: SkillSyncPaths = DEFAULT_SYNC_PATHS,
|
|
989
|
-
|
|
1182
|
+
options: { previouslyApplied?: readonly string[]; expectedFolderHash?: string; legacyBaseline?: boolean } = {},
|
|
1183
|
+
): Promise<SkillWriteOutcome | false> {
|
|
1184
|
+
if (isNativeSkill(skill)) {
|
|
1185
|
+
assertNativePull({ skills: [{ ...skill, name: 'native-' + skill.id }], skill_sync_protocol: 2, sync_scope: skill.native_scope,
|
|
1186
|
+
sync_enabled: true, last_synced_at: null });
|
|
1187
|
+
if (bundleBytes?.length) throw new Error('Native Skills require their verified complete file response.');
|
|
1188
|
+
}
|
|
990
1189
|
const skillDir = path.join(
|
|
991
1190
|
paths.skillsDir,
|
|
992
1191
|
safeName(skill.name, paths.skillsDir),
|
|
993
1192
|
);
|
|
1193
|
+
const preserve = { previouslyApplied: options.previouslyApplied, expectedFolderHash: options.expectedFolderHash,
|
|
1194
|
+
native: isNativeSkill(skill),
|
|
1195
|
+
// An unchanged legacy folder upgrading in place: shared files take the cloud version.
|
|
1196
|
+
legacyBaseline: isNativeSkill(skill) && options.legacyBaseline === true && options.expectedFolderHash !== undefined };
|
|
994
1197
|
|
|
995
1198
|
if (bundleBytes?.length) {
|
|
996
1199
|
const bundlePath = path.join(
|
|
@@ -999,8 +1202,11 @@ export async function writeCloudSkillToDisk(
|
|
|
999
1202
|
);
|
|
1000
1203
|
try {
|
|
1001
1204
|
await fs.writeFile(bundlePath, bundleBytes);
|
|
1002
|
-
|
|
1003
|
-
|
|
1205
|
+
let appliedFiles: string[] = [];
|
|
1206
|
+
await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
|
|
1207
|
+
appliedFiles = await extractZipToDirectory(bundlePath, stagingDir);
|
|
1208
|
+
}, preserve);
|
|
1209
|
+
return { written: true, appliedFiles };
|
|
1004
1210
|
} finally {
|
|
1005
1211
|
await fs.rm(bundlePath, { force: true });
|
|
1006
1212
|
}
|
|
@@ -1019,6 +1225,7 @@ export async function writeCloudSkillToDisk(
|
|
|
1019
1225
|
`Synced bundle for "${skill.name}" is missing SKILL.md or SKILLS.md`,
|
|
1020
1226
|
);
|
|
1021
1227
|
}
|
|
1228
|
+
const appliedFiles: string[] = [];
|
|
1022
1229
|
await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
|
|
1023
1230
|
for (const bundleFile of skill.bundle_files || []) {
|
|
1024
1231
|
const filePath = resolveSkillBundleFilePath(
|
|
@@ -1030,9 +1237,18 @@ export async function writeCloudSkillToDisk(
|
|
|
1030
1237
|
filePath,
|
|
1031
1238
|
Buffer.from(bundleFile.content_b64, "base64"),
|
|
1032
1239
|
);
|
|
1240
|
+
appliedFiles.push(toPosixRelativePath(stagingDir, filePath));
|
|
1033
1241
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1242
|
+
if (skill.native_reference) {
|
|
1243
|
+
const identity: NativeSkillIdentity = { schema: 1, cloudId: skill.id, reference: skill.native_reference,
|
|
1244
|
+
displayName: skill.display_name || skill.name, frontmatterName: nativeFrontmatterName(skill.skill_md || ''),
|
|
1245
|
+
scope: skill.native_scope!, folderHash: await computeFolderHash(stagingDir),
|
|
1246
|
+
files: Object.fromEntries((skill.bundle_files || []).map(file => [file.path, nativeFileDigest(Buffer.from(file.content_b64, 'base64'))])) };
|
|
1247
|
+
await fs.writeFile(path.join(stagingDir, NATIVE_IDENTITY_FILE), JSON.stringify(identity), { mode: 0o600 });
|
|
1248
|
+
appliedFiles.push(NATIVE_IDENTITY_FILE);
|
|
1249
|
+
}
|
|
1250
|
+
}, preserve);
|
|
1251
|
+
return { written: true, appliedFiles: appliedFiles.sort() };
|
|
1036
1252
|
}
|
|
1037
1253
|
|
|
1038
1254
|
await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
|
|
@@ -1041,6 +1257,6 @@ export async function writeCloudSkillToDisk(
|
|
|
1041
1257
|
skill.skill_md ?? "",
|
|
1042
1258
|
"utf8",
|
|
1043
1259
|
);
|
|
1044
|
-
});
|
|
1045
|
-
return true;
|
|
1260
|
+
}, preserve);
|
|
1261
|
+
return { written: true, appliedFiles: ["SKILL.md"] };
|
|
1046
1262
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { CloudSkill, NativeSkillReference, SyncedSkill, SyncPullResponse } from './types';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
export const NATIVE_IDENTITY_FILE = '.notis-native-skill.json';
|
|
5
|
+
export interface NativeSkillIdentity {
|
|
6
|
+
schema: 1;
|
|
7
|
+
cloudId: string;
|
|
8
|
+
reference: NativeSkillReference;
|
|
9
|
+
displayName: string;
|
|
10
|
+
frontmatterName: string;
|
|
11
|
+
scope: string;
|
|
12
|
+
folderHash: string;
|
|
13
|
+
files: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
16
|
+
|
|
17
|
+
export function checkedNativeReference(value: unknown, cloudId?: string): NativeSkillReference {
|
|
18
|
+
const reference = value as NativeSkillReference;
|
|
19
|
+
if (!reference || typeof reference !== 'object' || Array.isArray(reference)
|
|
20
|
+
|| Object.keys(reference).sort().join(',') !== 'access_revision,kind,skill_id,version'
|
|
21
|
+
|| reference.kind !== 'native_standalone_skill' || !UUID.test(reference.skill_id)
|
|
22
|
+
|| (cloudId !== undefined && reference.skill_id !== cloudId)
|
|
23
|
+
|| !Number.isSafeInteger(reference.access_revision) || reference.access_revision < 0
|
|
24
|
+
|| !reference.version || Object.keys(reference.version).sort().join(',') !== 'digest,revision'
|
|
25
|
+
|| !Number.isSafeInteger(reference.version.revision) || reference.version.revision < 1
|
|
26
|
+
|| !/^[a-f0-9]{64}$/.test(reference.version.digest)) {
|
|
27
|
+
throw new Error('Native Skill identity is unavailable. Refresh before syncing.');
|
|
28
|
+
}
|
|
29
|
+
return structuredClone(reference);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function nativeDirectory(reference: NativeSkillReference): string {
|
|
33
|
+
return 'native-' + checkedNativeReference(reference).skill_id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sameNativeReference(left?: NativeSkillReference, right?: NativeSkillReference): boolean {
|
|
37
|
+
return Boolean(left && right && left.skill_id === right.skill_id
|
|
38
|
+
&& left.access_revision === right.access_revision && left.version.revision === right.version.revision
|
|
39
|
+
&& left.version.digest === right.version.digest);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isNativeSkill(skill: CloudSkill): boolean {
|
|
43
|
+
return skill.source === 'native' || skill.native_reference !== undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function assertNativePull(response: SyncPullResponse): void {
|
|
47
|
+
const ids = new Set<string>();
|
|
48
|
+
for (const skill of response.skills) {
|
|
49
|
+
if (!isNativeSkill(skill)) continue;
|
|
50
|
+
const reference = checkedNativeReference(skill.native_reference, skill.id);
|
|
51
|
+
if (response.skill_sync_protocol !== 2 || skill.name !== nativeDirectory(reference) || ids.has(skill.id)
|
|
52
|
+
|| !/^[a-f0-9]{64}$/.test(response.sync_scope || '') || skill.native_scope !== response.sync_scope
|
|
53
|
+
|| !Array.isArray(skill.bundle_files) || skill.bundle_hydration_failed || typeof skill.skill_md !== 'string') {
|
|
54
|
+
throw new Error('Native Skill inventory is incomplete. Existing local files were retained.');
|
|
55
|
+
}
|
|
56
|
+
const paths = new Set<string>();
|
|
57
|
+
let entrypoint = false;
|
|
58
|
+
for (const file of skill.bundle_files) {
|
|
59
|
+
if (typeof file.path !== 'string' || typeof file.content_b64 !== 'string'
|
|
60
|
+
|| file.path.startsWith('/') || file.path.includes('\\') || file.path.includes('\0')
|
|
61
|
+
|| file.path.split('/').some(part => !part || part === '.' || part === '..')
|
|
62
|
+
|| paths.has(file.path.toLowerCase()) || file.path.toLowerCase() === NATIVE_IDENTITY_FILE) {
|
|
63
|
+
throw new Error('Native Skill bundle has a conflicting managed path.');
|
|
64
|
+
}
|
|
65
|
+
paths.add(file.path.toLowerCase());
|
|
66
|
+
if (file.path === 'SKILL.md') {
|
|
67
|
+
entrypoint = Buffer.from(file.content_b64, 'base64').equals(Buffer.from(skill.skill_md, 'utf8'));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!entrypoint) throw new Error('Native Skill instructions and files do not match.');
|
|
71
|
+
ids.add(skill.id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function nativeFileDigest(bytes: Buffer): string {
|
|
76
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function nativeFrontmatterName(markdown: string): string {
|
|
80
|
+
const header = markdown.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
81
|
+
return header?.[1].match(/^name\s*:\s*(.+)$/m)?.[1].trim().replace(/^['"]|['"]$/g, '') || '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function checkedNativeIdentity(value: unknown): NativeSkillIdentity {
|
|
85
|
+
const identity = value as NativeSkillIdentity;
|
|
86
|
+
if (!identity || identity.schema !== 1 || typeof identity.cloudId !== 'string'
|
|
87
|
+
|| typeof identity.displayName !== 'string' || typeof identity.frontmatterName !== 'string'
|
|
88
|
+
|| !/^[a-f0-9]{64}$/.test(identity.scope) || !/^[a-f0-9]{64}$/.test(identity.folderHash)
|
|
89
|
+
|| !identity.files || !Object.keys(identity.files).includes('SKILL.md')
|
|
90
|
+
|| Object.entries(identity.files).some(([name, hash]) => name.startsWith('/') || name.includes('\\')
|
|
91
|
+
|| name.split('/').some(part => !part || part === '.' || part === '..') || !/^[a-f0-9]{64}$/.test(hash))) {
|
|
92
|
+
throw new Error('Native Skill provenance is incomplete. Existing files were retained.');
|
|
93
|
+
}
|
|
94
|
+
return { ...identity, reference: checkedNativeReference(identity.reference, identity.cloudId) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function nativeState(skill: CloudSkill): Partial<SyncedSkill> {
|
|
98
|
+
if (!isNativeSkill(skill)) return {};
|
|
99
|
+
return { nativeReference: checkedNativeReference(skill.native_reference, skill.id),
|
|
100
|
+
...(skill.native_settings ? { nativeSettingsRevision: skill.native_settings.revision } : {}),
|
|
101
|
+
nativeDisplayName: skill.display_name || skill.name, nativeScope: skill.native_scope, nativeUnavailable: false };
|
|
102
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { CloudSkill, LocalSkill, NotisSyncState, SyncPullResponse } from './types';
|
|
2
|
+
import { checkedNativeReference, isNativeSkill, sameNativeReference } from './native-identity';
|
|
3
|
+
|
|
4
|
+
/** Folder names may change; existing managed cloud identity may not. */
|
|
5
|
+
export function bindNativeFolders(response: SyncPullResponse, locals: LocalSkill[], state: NotisSyncState): SyncPullResponse {
|
|
6
|
+
const nativeIds = new Set(response.skills.filter(isNativeSkill).map(skill => skill.id));
|
|
7
|
+
for (const entry of Object.values(state.skills)) if (entry.nativeReference || entry.nativeUnavailable) nativeIds.add(entry.cloudId);
|
|
8
|
+
const byId = new Map<string, LocalSkill>();
|
|
9
|
+
for (const local of locals) {
|
|
10
|
+
const id = local.cloudId || state.skills[local.name]?.cloudId;
|
|
11
|
+
if (!id || (!nativeIds.has(id) && !local.nativeReference)) continue;
|
|
12
|
+
if (local.nativeReference) checkedNativeReference(local.nativeReference, id);
|
|
13
|
+
if (byId.has(id)) throw new Error('Two local folders claim the same native Skill. Keep both copies and resolve the conflict.');
|
|
14
|
+
byId.set(id, local);
|
|
15
|
+
}
|
|
16
|
+
const skills = response.skills.map(skill => isNativeSkill(skill) && byId.has(skill.id)
|
|
17
|
+
? { ...skill, name: byId.get(skill.id)!.name } : skill);
|
|
18
|
+
for (const skill of skills.filter(isNativeSkill)) {
|
|
19
|
+
if (skills.some(other => other.id !== skill.id && other.name === skill.name)) {
|
|
20
|
+
throw new Error('A native Skill and another Skill share a local folder. Existing files were retained.');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { ...response, skills };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function previousNativeState(state: NotisSyncState, skill: CloudSkill) {
|
|
27
|
+
const named = state.skills[skill.name];
|
|
28
|
+
return named?.cloudId === skill.id ? named : Object.values(state.skills).find(entry => entry.cloudId === skill.id);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function nativeLocalReady(skill: CloudSkill, local?: LocalSkill): boolean {
|
|
32
|
+
return Boolean(local?.nativeFilesCurrent && local.folderHash === local.nativeBaseFolderHash
|
|
33
|
+
&& local.cloudId === skill.id && local.nativeScope === skill.native_scope
|
|
34
|
+
&& sameNativeReference(local.nativeReference, skill.native_reference));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A pre-native folder (no identity marker, no native state) the cloud now serves
|
|
38
|
+
* as a native Skill. It stays usable as before until its upgrade write lands. */
|
|
39
|
+
export function pendingLegacyUpgrade(skill: CloudSkill, local: LocalSkill | undefined, state: NotisSyncState): boolean {
|
|
40
|
+
if (!isNativeSkill(skill) || !local || local.nativeReference || local.cloudId) return false;
|
|
41
|
+
const previous = previousNativeState(state, skill);
|
|
42
|
+
return Boolean(previous && !previous.nativeReference);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function nativeLinkRows(response: SyncPullResponse, locals: LocalSkill[], state?: NotisSyncState): CloudSkill[] {
|
|
46
|
+
return response.skills.map(skill => {
|
|
47
|
+
const local = locals.find(row => row.name === skill.name);
|
|
48
|
+
return isNativeSkill(skill) && !nativeLocalReady(skill, local) && !(state && pendingLegacyUpgrade(skill, local, state))
|
|
49
|
+
? { ...skill, status: 'disabled' } : skill;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function retainMissingNativeState(previous: NotisSyncState, next: NotisSyncState): NotisSyncState {
|
|
54
|
+
const present = new Set(Object.values(next.skills).map(row => row.cloudId));
|
|
55
|
+
const retained = Object.fromEntries(Object.entries(previous.skills)
|
|
56
|
+
.filter(([, row]) => (row.nativeReference || row.nativeUnavailable) && !present.has(row.cloudId))
|
|
57
|
+
.map(([name, row]) => [name, { ...row, nativeUnavailable: true, verifiedAgentLinks: {} }]));
|
|
58
|
+
const history = structuredClone(previous.nativeHistory || {});
|
|
59
|
+
for (const [name, row] of [...Object.entries(previous.skills), ...Object.entries(next.skills)]) {
|
|
60
|
+
if (!row.nativeReference && !row.nativeUnavailable) continue;
|
|
61
|
+
const names = history[row.cloudId]?.names || [];
|
|
62
|
+
history[row.cloudId] = { names: [...new Set([...names, name])] };
|
|
63
|
+
}
|
|
64
|
+
return { ...next, skills: { ...retained, ...next.skills },
|
|
65
|
+
...(Object.keys(history).length ? { nativeHistory: history } : {}) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function hasNativeFolderHistory(state: NotisSyncState, name: string): boolean {
|
|
69
|
+
return Object.values(state.nativeHistory || {}).some(row => row.names.includes(name));
|
|
70
|
+
}
|