@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
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* is already running has no business handing the same work back to Notis, and
|
|
6
6
|
* nothing about the loop it would create is self-limiting.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* message at the point of the mistake
|
|
12
|
-
*
|
|
8
|
+
* The guard covers the friendly command only. Agents start and steer threads
|
|
9
|
+
* on purpose through `LOCAL_NOTIS_CONTROL_THREADS`, so `notis tools exec` is
|
|
10
|
+
* not refused here and the server applies no extra check. What this adds is a
|
|
11
|
+
* clear message at the point of the common mistake: an agent running the
|
|
12
|
+
* hand-over its own instructions describe.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { existsSync } from 'node:fs';
|
|
@@ -50,19 +50,3 @@ export function assertNotDelegated(commandLabel, env = process.env) {
|
|
|
50
50
|
],
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Tools that hand work to a Notis agent, by canonical name.
|
|
56
|
-
*
|
|
57
|
-
* `notis handover` is not the only way to reach these: `notis tools exec` takes
|
|
58
|
-
* any tool name, and `tools exec-parallel` takes a list of them. Guarding only
|
|
59
|
-
* the friendly command would leave the escape hatch it exists to wrap.
|
|
60
|
-
*/
|
|
61
|
-
export const HANDOVER_TOOL_NAMES = new Set(['LOCAL_NOTIS_HAND_OVER']);
|
|
62
|
-
|
|
63
|
-
export function assertToolNotDelegated(toolName, env = process.env) {
|
|
64
|
-
if (!HANDOVER_TOOL_NAMES.has(String(toolName || '').toUpperCase())) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
assertNotDelegated(`tools exec ${toolName}`, env);
|
|
68
|
-
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { open } from 'node:fs/promises';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { basename, extname } from 'node:path';
|
|
4
|
+
import { httpRequest } from './transport.js';
|
|
5
|
+
import { usageError } from './errors.js';
|
|
6
|
+
|
|
7
|
+
const MAX_BYTES = 8 * 1024 * 1024;
|
|
8
|
+
const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
|
|
9
|
+
const MIME = { '.html': 'text/html', '.htm': 'text/html', '.txt': 'text/plain', '.md': 'text/markdown',
|
|
10
|
+
'.json': 'application/json', '.pdf': 'application/pdf', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp' };
|
|
11
|
+
|
|
12
|
+
/** Reads a bounded local file, never interprets it and never attaches it to a row. */
|
|
13
|
+
export async function uploadNativeRecordFileWithCliRuntime({ runtime, target, recordKey, schemaRevision, requestId,
|
|
14
|
+
filePath, name = basename(filePath || ''), contentType = MIME[extname(filePath || '').toLowerCase()] || 'application/octet-stream',
|
|
15
|
+
signal, request = httpRequest }) {
|
|
16
|
+
if (!UUID.test(recordKey || '') || !UUID.test(requestId || '') || !Number.isSafeInteger(schemaRevision) || schemaRevision < 0
|
|
17
|
+
|| !target || typeof target !== 'object' || Array.isArray(target) || !filePath) {
|
|
18
|
+
throw usageError('Choose a local file, native target, record key, schema revision and stable request ID.');
|
|
19
|
+
}
|
|
20
|
+
const handle = await open(filePath, 'r');
|
|
21
|
+
let data;
|
|
22
|
+
try {
|
|
23
|
+
const stat = await handle.stat();
|
|
24
|
+
if (!stat.isFile() || stat.size < 1 || stat.size > MAX_BYTES) throw usageError('Choose a regular file up to 8 MB.');
|
|
25
|
+
const buffer = Buffer.alloc(MAX_BYTES + 1);
|
|
26
|
+
let length = 0;
|
|
27
|
+
while (length < buffer.length) {
|
|
28
|
+
const read = await handle.read(buffer, length, buffer.length - length, length);
|
|
29
|
+
if (!read.bytesRead) break;
|
|
30
|
+
length += read.bytesRead;
|
|
31
|
+
}
|
|
32
|
+
if (!length || length > MAX_BYTES) throw usageError('Choose a file up to 8 MB.');
|
|
33
|
+
data = buffer.subarray(0, length);
|
|
34
|
+
} finally { await handle.close(); }
|
|
35
|
+
const digest = createHash('sha256').update(data).digest('hex');
|
|
36
|
+
const body = { protocol: 1, target, record_key: recordKey, schema_revision: schemaRevision, request_id: requestId,
|
|
37
|
+
name, content_type: contentType, base64: data.toString('base64') };
|
|
38
|
+
// No automatic retry: repeating this command with the same request ID is the
|
|
39
|
+
// exact immutable intent; a mismatched request fails instead of overwriting.
|
|
40
|
+
const result = (await request({ runtime, method: 'POST', path: '/portal_native_files/upload', spacesProtocol: true, body, signal })).payload;
|
|
41
|
+
if (result?.protocol !== 1 || result.record_key !== recordKey || result.request_id !== requestId
|
|
42
|
+
|| result.sha256 !== digest || result.size_bytes !== data.length || result.file?.name !== name
|
|
43
|
+
|| typeof result.file?.url !== 'string' || !/^https?:\/\//.test(result.file.url) || result.url !== result.file.url) {
|
|
44
|
+
throw usageError('The file upload is not confirmed. Repeat the same request ID and file bytes.');
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
package/src/runtime/oauth.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
randomBytes,
|
|
4
4
|
timingSafeEqual,
|
|
5
5
|
} from 'node:crypto';
|
|
6
|
+
import { subscribe, unsubscribe } from 'node:diagnostics_channel';
|
|
6
7
|
import {
|
|
7
8
|
mkdirSync,
|
|
8
9
|
readdirSync,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
rmdirSync,
|
|
12
13
|
rmSync,
|
|
13
14
|
statSync,
|
|
15
|
+
utimesSync,
|
|
14
16
|
writeFileSync,
|
|
15
17
|
} from 'node:fs';
|
|
16
18
|
import { createServer } from 'node:http';
|
|
@@ -21,7 +23,8 @@ import { execFileSync, spawn } from 'node:child_process';
|
|
|
21
23
|
import { createInterface } from 'node:readline/promises';
|
|
22
24
|
|
|
23
25
|
import { CliError, EXIT_CODES, usageError } from './errors.js';
|
|
24
|
-
import {
|
|
26
|
+
import { readComputerIdentity } from './computer-identity.js';
|
|
27
|
+
import { getAuthRecovery, getRetryRecovery, quoteShellArgument } from './auth-recovery.js';
|
|
25
28
|
import {
|
|
26
29
|
channelFromProfile,
|
|
27
30
|
cliCommandForChannel,
|
|
@@ -38,19 +41,48 @@ import {
|
|
|
38
41
|
updateConfig,
|
|
39
42
|
} from './profiles.js';
|
|
40
43
|
|
|
44
|
+
// A login without --scope asks for every permission the server advertises for
|
|
45
|
+
// the CLI resource, so a permission added on the server reaches the next sign-in
|
|
46
|
+
// without a CLI release. This list only stands in for a server that advertises
|
|
47
|
+
// none.
|
|
41
48
|
export const DEFAULT_CLI_OAUTH_SCOPES = [
|
|
42
49
|
'notis:read',
|
|
43
50
|
'notis:write',
|
|
44
51
|
'notis:connections',
|
|
45
|
-
'notis:
|
|
52
|
+
'notis:spaces',
|
|
46
53
|
];
|
|
47
|
-
|
|
54
|
+
|
|
55
|
+
export function defaultCliOAuthScopes(metadata) {
|
|
56
|
+
const advertised = (metadata?.scopesSupported || [])
|
|
57
|
+
.filter((scope) => typeof scope === 'string' && scope.startsWith('notis:'));
|
|
58
|
+
return advertised.length > 0 ? [...new Set(advertised)] : DEFAULT_CLI_OAUTH_SCOPES;
|
|
59
|
+
}
|
|
60
|
+
// Read at call time so a test, or a caller with its own HOME, never touches
|
|
61
|
+
// the lock another process on this computer is using.
|
|
62
|
+
function oauthLockDir() {
|
|
63
|
+
return join(homedir(), '.notis', 'oauth.lock');
|
|
64
|
+
}
|
|
48
65
|
const DEFAULT_LOGIN_TIMEOUT_MS = 300_000;
|
|
49
66
|
const DEFAULT_REFRESH_EXPIRES_IN = 30 * 24 * 60 * 60;
|
|
50
67
|
// A parked authorization outlives the terminal that started it: the user may
|
|
51
68
|
// still have to sign up, verify an email, and consent before pasting the code.
|
|
52
69
|
const PENDING_LOGIN_TTL_SECONDS = 30 * 60;
|
|
53
70
|
const OAUTH_HTTP_TIMEOUT_MS = 10_000;
|
|
71
|
+
// A refresh rotates the token it presents as soon as the server reads it, so
|
|
72
|
+
// the token request is budgeted by phase (docs/no-dead-ends.md). Reaching the
|
|
73
|
+
// server costs nothing: a slow or stalled link (a VPN over a phone hotspot)
|
|
74
|
+
// may take this long, and a request never written is retried. Once written,
|
|
75
|
+
// the server has very likely already answered, so the client waits this long
|
|
76
|
+
// for that answer instead of abandoning it and keeping a rotated token.
|
|
77
|
+
const OAUTH_TOKEN_SEND_TIMEOUT_MS = 30_000;
|
|
78
|
+
const OAUTH_TOKEN_RESPONSE_TIMEOUT_MS = 60_000;
|
|
79
|
+
const OAUTH_TOKEN_UNSENT_RETRY_DELAYS_MS = [1_000, 3_000];
|
|
80
|
+
// Another process waits for a refresh in progress rather than failing.
|
|
81
|
+
const OAUTH_REFRESH_LOCK_WAIT_MS = OAUTH_TOKEN_SEND_TIMEOUT_MS + OAUTH_TOKEN_RESPONSE_TIMEOUT_MS + 30_000;
|
|
82
|
+
// A refresh keeps its lock folders fresh so that no process, current or older,
|
|
83
|
+
// takes them over as stale while the token request is still running.
|
|
84
|
+
const OAUTH_REFRESH_LOCK_TOUCH_MS = 5_000;
|
|
85
|
+
const OAUTH_REFRESH_SAVE_RETRY_DELAYS_MS = [250, 1_000];
|
|
54
86
|
const RESPONSE_FLUSH_GRACE_MS = 2_000;
|
|
55
87
|
const MAX_NODE_TIMER_MS = 2_147_483_647;
|
|
56
88
|
|
|
@@ -112,6 +144,231 @@ async function fetchJson(url, init = {}, fetchImpl = fetch) {
|
|
|
112
144
|
}
|
|
113
145
|
}
|
|
114
146
|
|
|
147
|
+
// Token requests in flight, by the request id each one carries in a header.
|
|
148
|
+
// Node's fetch reports through these diagnostics channels when it creates a
|
|
149
|
+
// request and when it writes the request to a socket; the id ties each report
|
|
150
|
+
// to its request.
|
|
151
|
+
const tokenRequestPhases = new Map();
|
|
152
|
+
const REQUEST_CREATED_CHANNEL = 'undici:request:create';
|
|
153
|
+
const REQUEST_WRITTEN_CHANNEL = 'undici:client:sendHeaders';
|
|
154
|
+
// Failures that happen before a request can be written anywhere.
|
|
155
|
+
const UNSENT_FAILURE_CODES = new Set([
|
|
156
|
+
'ENOTFOUND',
|
|
157
|
+
'EAI_AGAIN',
|
|
158
|
+
'ECONNREFUSED',
|
|
159
|
+
'EHOSTUNREACH',
|
|
160
|
+
'ENETUNREACH',
|
|
161
|
+
'ENETDOWN',
|
|
162
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
function diagnosticHeaderText(value) {
|
|
166
|
+
if (typeof value === 'string') return value;
|
|
167
|
+
if (Array.isArray(value)) return value.map((item) => String(item)).join('\n');
|
|
168
|
+
return '';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function trackedTokenRequest(message) {
|
|
172
|
+
if (tokenRequestPhases.size === 0) return null;
|
|
173
|
+
const text = `${diagnosticHeaderText(message?.headers)}\n${diagnosticHeaderText(message?.request?.headers)}`;
|
|
174
|
+
for (const [requestId, phase] of tokenRequestPhases) {
|
|
175
|
+
if (text.includes(requestId)) return phase;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function onTokenRequestCreated(message) {
|
|
181
|
+
const phase = trackedTokenRequest(message);
|
|
182
|
+
if (phase) phase.created = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function onTokenRequestWritten(message) {
|
|
186
|
+
const phase = trackedTokenRequest(message);
|
|
187
|
+
if (phase && !phase.sent) {
|
|
188
|
+
phase.sent = true;
|
|
189
|
+
phase.onSent?.();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function watchTokenRequest(requestId) {
|
|
194
|
+
const phase = { created: false, sent: false, onSent: null };
|
|
195
|
+
if (tokenRequestPhases.size === 0) {
|
|
196
|
+
subscribe(REQUEST_CREATED_CHANNEL, onTokenRequestCreated);
|
|
197
|
+
subscribe(REQUEST_WRITTEN_CHANNEL, onTokenRequestWritten);
|
|
198
|
+
}
|
|
199
|
+
tokenRequestPhases.set(requestId, phase);
|
|
200
|
+
const stop = () => {
|
|
201
|
+
if (!tokenRequestPhases.delete(requestId)) return;
|
|
202
|
+
if (tokenRequestPhases.size === 0) {
|
|
203
|
+
unsubscribe(REQUEST_CREATED_CHANNEL, onTokenRequestCreated);
|
|
204
|
+
unsubscribe(REQUEST_WRITTEN_CHANNEL, onTokenRequestWritten);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
return { phase, stop };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function networkFailureCode(error) {
|
|
211
|
+
return error?.cause?.code || error?.cause?.errors?.[0]?.code || error?.code || null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
class TokenRequestFailure extends Error {
|
|
215
|
+
constructor(error, { sent, timedOut }) {
|
|
216
|
+
super(error instanceof Error ? error.message : String(error));
|
|
217
|
+
this.name = 'TokenRequestFailure';
|
|
218
|
+
this.cause = error;
|
|
219
|
+
this.sent = sent;
|
|
220
|
+
this.timedOut = timedOut;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function attemptTokenRequest(url, body, fetchImpl, { sendDeadline, responseTimeoutMs }) {
|
|
225
|
+
const requestId = `cli_${randomBytes(12).toString('hex')}`;
|
|
226
|
+
const { phase, stop } = watchTokenRequest(requestId);
|
|
227
|
+
const controller = new AbortController();
|
|
228
|
+
let answered = false;
|
|
229
|
+
let timedOut = false;
|
|
230
|
+
let timer = null;
|
|
231
|
+
// Timers run before the event loop reads sockets, so an answer that arrived
|
|
232
|
+
// just as a deadline passed is still waiting to be read. Let the loop read
|
|
233
|
+
// it first and give up only if it is still missing.
|
|
234
|
+
const giveUp = (stage) => setImmediate(() => {
|
|
235
|
+
if (answered || (stage === 'send' && phase.sent)) return;
|
|
236
|
+
timedOut = true;
|
|
237
|
+
controller.abort();
|
|
238
|
+
});
|
|
239
|
+
const waitForAnswer = () => {
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
timer = setTimeout(() => giveUp('answer'), responseTimeoutMs);
|
|
242
|
+
};
|
|
243
|
+
phase.onSent = waitForAnswer;
|
|
244
|
+
timer = setTimeout(() => {
|
|
245
|
+
if (phase.sent) return;
|
|
246
|
+
// Provably never written: stop waiting, nothing was consumed.
|
|
247
|
+
if (phase.created) giveUp('send');
|
|
248
|
+
// A fetch that does not report its writes may already have sent it.
|
|
249
|
+
else waitForAnswer();
|
|
250
|
+
}, Math.max(0, sendDeadline - Date.now()));
|
|
251
|
+
try {
|
|
252
|
+
const response = await fetchImpl(url, {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: {
|
|
255
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
256
|
+
'X-Request-Id': requestId,
|
|
257
|
+
},
|
|
258
|
+
body,
|
|
259
|
+
signal: controller.signal,
|
|
260
|
+
});
|
|
261
|
+
const text = await response.text();
|
|
262
|
+
answered = true;
|
|
263
|
+
return { response, text };
|
|
264
|
+
} catch (error) {
|
|
265
|
+
const code = networkFailureCode(error);
|
|
266
|
+
const sent = phase.sent || (!phase.created && !UNSENT_FAILURE_CODES.has(code));
|
|
267
|
+
throw new TokenRequestFailure(error, { sent, timedOut });
|
|
268
|
+
} finally {
|
|
269
|
+
clearTimeout(timer);
|
|
270
|
+
stop();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* POST to the token endpoint without abandoning an answer the server may
|
|
276
|
+
* already have acted on. A request that provably never reached the server is
|
|
277
|
+
* retried within the send budget; once written, a failure leaves the outcome
|
|
278
|
+
* unknown and is never replayed here (the server forgives a retry of the same
|
|
279
|
+
* token for a few minutes, so the next command recovers).
|
|
280
|
+
*/
|
|
281
|
+
async function postTokenRequest(url, body, fetchImpl = fetch, {
|
|
282
|
+
sendTimeoutMs = OAUTH_TOKEN_SEND_TIMEOUT_MS,
|
|
283
|
+
responseTimeoutMs = OAUTH_TOKEN_RESPONSE_TIMEOUT_MS,
|
|
284
|
+
unsentRetryDelaysMs = OAUTH_TOKEN_UNSENT_RETRY_DELAYS_MS,
|
|
285
|
+
} = {}) {
|
|
286
|
+
const sendDeadline = Date.now() + sendTimeoutMs;
|
|
287
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
288
|
+
try {
|
|
289
|
+
return await attemptTokenRequest(url, body, fetchImpl, { sendDeadline, responseTimeoutMs });
|
|
290
|
+
} catch (failure) {
|
|
291
|
+
const delay = unsentRetryDelaysMs[attempt];
|
|
292
|
+
if (failure.sent || delay === undefined || Date.now() + delay >= sendDeadline) throw failure;
|
|
293
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function refreshRetryError(runtime, code, message, cause) {
|
|
299
|
+
return new CliError({
|
|
300
|
+
code,
|
|
301
|
+
message,
|
|
302
|
+
exitCode: EXIT_CODES.network,
|
|
303
|
+
retryable: true,
|
|
304
|
+
hints: getRetryRecovery(runtime).hints,
|
|
305
|
+
cause,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// The answer to a refresh, or a retryable error that keeps the stored sign-in
|
|
310
|
+
// untouched when the answer is missing or unreadable.
|
|
311
|
+
function readRefreshAnswer(runtime, { response, text }) {
|
|
312
|
+
let payload = null;
|
|
313
|
+
try {
|
|
314
|
+
payload = JSON.parse(text);
|
|
315
|
+
} catch {
|
|
316
|
+
payload = null;
|
|
317
|
+
}
|
|
318
|
+
if (!response.ok) {
|
|
319
|
+
const details = payload && typeof payload === 'object' ? payload : {};
|
|
320
|
+
if (response.status >= 500 || response.status === 429) {
|
|
321
|
+
throw refreshRetryError(
|
|
322
|
+
runtime,
|
|
323
|
+
details.error || 'oauth_refresh_unavailable',
|
|
324
|
+
'The Notis sign-in server could not renew your session right now. Your sign-in is kept; run the command again.',
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
throw oauthError(
|
|
328
|
+
details.error || 'oauth_request_failed',
|
|
329
|
+
details.error_description || details.message || `OAuth request failed with status ${response.status}`,
|
|
330
|
+
null,
|
|
331
|
+
details,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (
|
|
335
|
+
!payload
|
|
336
|
+
|| typeof payload.access_token !== 'string'
|
|
337
|
+
|| !payload.access_token
|
|
338
|
+
|| typeof payload.refresh_token !== 'string'
|
|
339
|
+
|| !payload.refresh_token
|
|
340
|
+
) {
|
|
341
|
+
throw refreshRetryError(
|
|
342
|
+
runtime,
|
|
343
|
+
'oauth_refresh_outcome_unknown',
|
|
344
|
+
'The Notis sign-in server answered, but the renewed session could not be read. Your sign-in is kept; run the command again.',
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return payload;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function persistRefreshedSignIn(runtime, metadata, tokenResponse) {
|
|
351
|
+
let lastError = null;
|
|
352
|
+
for (const delay of [0, ...OAUTH_REFRESH_SAVE_RETRY_DELAYS_MS]) {
|
|
353
|
+
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
354
|
+
try {
|
|
355
|
+
return persistOAuthTokenResponse(runtime, metadata, tokenResponse);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
lastError = error;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Never revoke the renewed sign-in here: it is the only valid one left. The
|
|
361
|
+
// server forgives presenting the previous token again for a few minutes.
|
|
362
|
+
throw new CliError({
|
|
363
|
+
code: 'oauth_refresh_not_saved',
|
|
364
|
+
message: 'Your renewed sign-in could not be saved on this computer. Run the command again in the next few minutes.',
|
|
365
|
+
exitCode: EXIT_CODES.unexpected,
|
|
366
|
+
retryable: true,
|
|
367
|
+
hints: getRetryRecovery(runtime).hints,
|
|
368
|
+
cause: lastError,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
115
372
|
export async function discoverCliOAuth(apiBase, fetchImpl = fetch) {
|
|
116
373
|
const normalizedApiBase = apiBase.replace(/\/+$/, '');
|
|
117
374
|
const protectedResource = await fetchJson(
|
|
@@ -144,6 +401,9 @@ export async function discoverCliOAuth(apiBase, fetchImpl = fetch) {
|
|
|
144
401
|
apiBase: normalizedApiBase,
|
|
145
402
|
issuer,
|
|
146
403
|
resource: protectedResource.resource,
|
|
404
|
+
scopesSupported: Array.isArray(protectedResource.scopes_supported)
|
|
405
|
+
? protectedResource.scopes_supported
|
|
406
|
+
: [],
|
|
147
407
|
clientId: protectedResource.notis_cli_client_id || 'notis_cli',
|
|
148
408
|
copyPasteRedirectUri: protectedResource.notis_cli_copy_paste_redirect_uri,
|
|
149
409
|
// A deployment that predates channel advertising, or a local one with no
|
|
@@ -407,7 +667,7 @@ function connectedCallbackHtml({ portalOrigin }) {
|
|
|
407
667
|
<button class="button button-primary" id="download-desktop" type="button">Download Notis Desktop</button>
|
|
408
668
|
<a class="button" href="${escapeHtml(webAppUrl)}">Open Web App</a>
|
|
409
669
|
</div>
|
|
410
|
-
<p class="helper">The CLI stays connected even if you continue in the web app.</p>
|
|
670
|
+
<p class="helper">Desktop is available for macOS and Windows. The CLI stays connected even if you continue in the web app.</p>
|
|
411
671
|
</section>
|
|
412
672
|
<section class="next-step" id="desktop-ready-view" aria-labelledby="desktop-ready-title" hidden>
|
|
413
673
|
<div class="status"><span class="status-dot"></span> Download started</div>
|
|
@@ -433,7 +693,6 @@ function connectedCallbackHtml({ portalOrigin }) {
|
|
|
433
693
|
const resolveDownload = async () => {
|
|
434
694
|
const ua = navigator.userAgent || '';
|
|
435
695
|
if (/Windows/i.test(ua)) return downloads.base + '/win32/x64/notis-x64.exe';
|
|
436
|
-
if (/Linux/i.test(ua) && !/Android/i.test(ua)) return downloads.base + '/linux/x64/notis-linux-x64.zip';
|
|
437
696
|
if (/Macintosh|Mac OS X/i.test(ua)) {
|
|
438
697
|
let architecture = '';
|
|
439
698
|
try {
|
|
@@ -450,6 +709,7 @@ function connectedCallbackHtml({ portalOrigin }) {
|
|
|
450
709
|
const startDownload = async () => {
|
|
451
710
|
const downloadUrl = await resolveDownload();
|
|
452
711
|
window.open(downloadUrl, '_blank', 'noopener,noreferrer');
|
|
712
|
+
if (downloadUrl === downloads.fallback) return;
|
|
453
713
|
connectedView.hidden = true;
|
|
454
714
|
readyView.hidden = false;
|
|
455
715
|
document.title = 'Quick login to Notis Desktop';
|
|
@@ -674,7 +934,19 @@ function storedOAuthMetadata(runtime, profile) {
|
|
|
674
934
|
};
|
|
675
935
|
}
|
|
676
936
|
|
|
677
|
-
|
|
937
|
+
// This computer's id and name, so the server keeps one CLI sign-in per
|
|
938
|
+
// computer and lists it as "Notis CLI on <computer>". Best effort: a sign-in
|
|
939
|
+
// without them still works and maps to the shared legacy grant.
|
|
940
|
+
function cliDeviceParams() {
|
|
941
|
+
try {
|
|
942
|
+
const identity = readComputerIdentity({ configFile: resolveConfigFile() });
|
|
943
|
+
return { device_id: identity.computerId, device_name: identity.displayName };
|
|
944
|
+
} catch {
|
|
945
|
+
return {};
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export function buildAuthorizeUrl(metadata, {
|
|
678
950
|
redirectUri,
|
|
679
951
|
challenge,
|
|
680
952
|
state,
|
|
@@ -689,6 +961,9 @@ function buildAuthorizeUrl(metadata, {
|
|
|
689
961
|
url.searchParams.set('code_challenge', challenge);
|
|
690
962
|
url.searchParams.set('code_challenge_method', 'S256');
|
|
691
963
|
url.searchParams.set('state', state);
|
|
964
|
+
for (const [key, value] of Object.entries(cliDeviceParams())) {
|
|
965
|
+
url.searchParams.set(key, value);
|
|
966
|
+
}
|
|
692
967
|
return url.toString();
|
|
693
968
|
}
|
|
694
969
|
|
|
@@ -1969,7 +2244,7 @@ export async function loginWithOAuth(
|
|
|
1969
2244
|
const metadata = await discoverCliOAuth(runtime.apiBase, fetchImpl);
|
|
1970
2245
|
const scopes = options.scope?.length
|
|
1971
2246
|
? [...new Set(options.scope)]
|
|
1972
|
-
:
|
|
2247
|
+
: defaultCliOAuthScopes(metadata);
|
|
1973
2248
|
const timeoutMs = requestedTimeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS;
|
|
1974
2249
|
// A detached listener is not a terminal waiting on a prompt: nobody is held
|
|
1975
2250
|
// up by it, and the user still has to sign up and verify an email, so its
|
|
@@ -2393,18 +2668,18 @@ function pendingAuthorizationProfiles() {
|
|
|
2393
2668
|
|
|
2394
2669
|
function lockIsStale() {
|
|
2395
2670
|
try {
|
|
2396
|
-
return Date.now() - statSync(
|
|
2671
|
+
return Date.now() - statSync(oauthLockDir()).mtimeMs > 45_000;
|
|
2397
2672
|
} catch {
|
|
2398
2673
|
return false;
|
|
2399
2674
|
}
|
|
2400
2675
|
}
|
|
2401
2676
|
|
|
2402
|
-
async function acquireRefreshLock(runtime, waitMs =
|
|
2677
|
+
async function acquireRefreshLock(runtime, waitMs = OAUTH_REFRESH_LOCK_WAIT_MS) {
|
|
2403
2678
|
mkdirSync(join(homedir(), '.notis'), { recursive: true });
|
|
2404
2679
|
const deadline = Date.now() + waitMs;
|
|
2405
2680
|
for (;;) {
|
|
2406
2681
|
try {
|
|
2407
|
-
mkdirSync(
|
|
2682
|
+
mkdirSync(oauthLockDir());
|
|
2408
2683
|
return true;
|
|
2409
2684
|
} catch (error) {
|
|
2410
2685
|
if (error?.code !== 'EEXIST') throw error;
|
|
@@ -2419,7 +2694,7 @@ async function acquireRefreshLock(runtime, waitMs = 60_000) {
|
|
|
2419
2694
|
}
|
|
2420
2695
|
if (lockIsStale()) {
|
|
2421
2696
|
try {
|
|
2422
|
-
rmdirSync(
|
|
2697
|
+
rmdirSync(oauthLockDir());
|
|
2423
2698
|
continue;
|
|
2424
2699
|
} catch {
|
|
2425
2700
|
// The owner may have completed between stat and removal.
|
|
@@ -2433,20 +2708,38 @@ async function acquireRefreshLock(runtime, waitMs = 60_000) {
|
|
|
2433
2708
|
}
|
|
2434
2709
|
}
|
|
2435
2710
|
|
|
2436
|
-
|
|
2711
|
+
// Touch lock folders while a refresh holds them, so their age keeps saying
|
|
2712
|
+
// "in use" for as long as the token request runs.
|
|
2713
|
+
function keepLockFoldersFresh(paths) {
|
|
2714
|
+
const touch = () => {
|
|
2715
|
+
const now = new Date();
|
|
2716
|
+
for (const path of paths) {
|
|
2717
|
+
try {
|
|
2718
|
+
utimesSync(path, now, now);
|
|
2719
|
+
} catch {
|
|
2720
|
+
// Released, or retired by stale-lock recovery; nothing to keep.
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
};
|
|
2724
|
+
const interval = setInterval(touch, OAUTH_REFRESH_LOCK_TOUCH_MS);
|
|
2725
|
+
interval.unref?.();
|
|
2726
|
+
return () => clearInterval(interval);
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
export async function refreshOAuthCredential(runtime, fetchImpl = fetch, tokenRequestOptions = {}) {
|
|
2437
2730
|
// Refresh rotates the same stored grant that login publishes and logout
|
|
2438
2731
|
// removes. Join their global -> profile lock order so a token rotation can
|
|
2439
2732
|
// never be mistaken for a successor authorization by a concurrent logout.
|
|
2440
|
-
const globalLock = await acquireListenerGlobalLock();
|
|
2733
|
+
const globalLock = await acquireListenerGlobalLock({ waitMs: OAUTH_REFRESH_LOCK_WAIT_MS });
|
|
2441
2734
|
let profileLock = null;
|
|
2442
2735
|
let ownsLock = false;
|
|
2736
|
+
let stopTouchingLocks = () => {};
|
|
2443
2737
|
let metadata = null;
|
|
2444
|
-
let rotatedResponse = null;
|
|
2445
|
-
let rotatedPersisted = false;
|
|
2446
2738
|
try {
|
|
2447
2739
|
profileLock = await acquireListenerStartLock(runtime);
|
|
2448
2740
|
ownsLock = await acquireRefreshLock(runtime);
|
|
2449
2741
|
if (!ownsLock) return true;
|
|
2742
|
+
stopTouchingLocks = keepLockFoldersFresh([profileLock, oauthLockDir()]);
|
|
2450
2743
|
const config = loadConfig();
|
|
2451
2744
|
const profile = getProfile(config, runtime.profileName);
|
|
2452
2745
|
assertOAuthApiTarget(runtime, profile);
|
|
@@ -2463,28 +2756,43 @@ export async function refreshOAuthCredential(runtime, fetchImpl = fetch) {
|
|
|
2463
2756
|
}
|
|
2464
2757
|
|
|
2465
2758
|
metadata = storedOAuthMetadata(runtime, profile);
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
body: new URLSearchParams({
|
|
2759
|
+
let answer;
|
|
2760
|
+
try {
|
|
2761
|
+
answer = await postTokenRequest(
|
|
2762
|
+
metadata.tokenEndpoint,
|
|
2763
|
+
new URLSearchParams({
|
|
2472
2764
|
grant_type: 'refresh_token',
|
|
2473
2765
|
refresh_token: profile.oauth_refresh_token,
|
|
2474
2766
|
client_id: profile.oauth_client_id,
|
|
2475
2767
|
resource: metadata.resource,
|
|
2768
|
+
...cliDeviceParams(),
|
|
2476
2769
|
}),
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2770
|
+
fetchImpl,
|
|
2771
|
+
tokenRequestOptions,
|
|
2772
|
+
);
|
|
2773
|
+
} catch (failure) {
|
|
2774
|
+
if (!(failure instanceof TokenRequestFailure)) throw failure;
|
|
2775
|
+
// The stored sign-in stays exactly as it was: either the server never
|
|
2776
|
+
// saw this request, or it forgives the same token again for a while.
|
|
2777
|
+
throw failure.sent
|
|
2778
|
+
? refreshRetryError(
|
|
2779
|
+
runtime,
|
|
2780
|
+
'oauth_refresh_outcome_unknown',
|
|
2781
|
+
'The Notis sign-in server did not answer in time. Your sign-in is kept; run the command again.',
|
|
2782
|
+
failure.cause,
|
|
2783
|
+
)
|
|
2784
|
+
: refreshRetryError(
|
|
2785
|
+
runtime,
|
|
2786
|
+
'oauth_refresh_unreachable',
|
|
2787
|
+
'Could not reach the Notis sign-in server. Your sign-in is kept; check the connection and run the command again.',
|
|
2788
|
+
failure.cause,
|
|
2789
|
+
);
|
|
2790
|
+
}
|
|
2791
|
+
const rotatedResponse = readRefreshAnswer(runtime, answer);
|
|
2792
|
+
const updated = await persistRefreshedSignIn(runtime, metadata, rotatedResponse);
|
|
2482
2793
|
updateRuntimeFromOAuthProfile(runtime, updated);
|
|
2483
2794
|
return true;
|
|
2484
2795
|
} catch (error) {
|
|
2485
|
-
if (rotatedResponse && !rotatedPersisted) {
|
|
2486
|
-
await revokeCancelledToken(metadata, rotatedResponse, fetchImpl);
|
|
2487
|
-
}
|
|
2488
2796
|
if (error instanceof CliError) {
|
|
2489
2797
|
throw new CliError({
|
|
2490
2798
|
code: error.code,
|
|
@@ -2492,16 +2800,19 @@ export async function refreshOAuthCredential(runtime, fetchImpl = fetch) {
|
|
|
2492
2800
|
exitCode: error.exitCode,
|
|
2493
2801
|
retryable: error.retryable,
|
|
2494
2802
|
details: error.details,
|
|
2495
|
-
|
|
2803
|
+
// A retryable failure keeps the sign-in, so its hint is to try again,
|
|
2804
|
+
// never to sign in again.
|
|
2805
|
+
hints: error.retryable ? error.hints : getAuthRecovery(runtime).hints,
|
|
2496
2806
|
warnings: error.warnings,
|
|
2497
2807
|
cause: error,
|
|
2498
2808
|
});
|
|
2499
2809
|
}
|
|
2500
2810
|
throw error;
|
|
2501
2811
|
} finally {
|
|
2812
|
+
stopTouchingLocks();
|
|
2502
2813
|
if (ownsLock) {
|
|
2503
2814
|
try {
|
|
2504
|
-
rmdirSync(
|
|
2815
|
+
rmdirSync(oauthLockDir());
|
|
2505
2816
|
} catch {
|
|
2506
2817
|
// A process exit or external cleanup may already have removed the lock.
|
|
2507
2818
|
}
|