@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.
Files changed (395) hide show
  1. package/README.md +542 -146
  2. package/bin/check-runtime.js +15 -0
  3. package/bin/notis.js +2 -0
  4. package/config/notis_app_design_rules.json +320 -0
  5. package/config/portal-route-roots.json +45 -0
  6. package/dist/agent-hooks/notis-agent-hook.mjs +42325 -18273
  7. package/dist/base-skills/notis-cli/.notis-base-skill.json +1 -0
  8. package/dist/base-skills/notis-cli/SKILL.md +51 -280
  9. package/dist/base-skills/notis-cli/references/app-delivery.md +17 -0
  10. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  11. package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
  12. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  13. package/dist/base-skills/notis-cli/references/troubleshooting.md +67 -0
  14. package/dist/base-skills/notis-query/.notis-base-skill.json +1 -0
  15. package/dist/base-skills/notis-query/SKILL.md +35 -699
  16. package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
  17. package/dist/base-skills/notis-query/references/documents.md +74 -0
  18. package/dist/base-skills/notis-query/references/query.md +76 -0
  19. package/dist/base-skills/notis-spaces/.notis-base-skill.json +1 -0
  20. package/dist/base-skills/notis-spaces/SKILL.md +218 -0
  21. package/dist/base-skills/notis-spaces/references/architecture.md +116 -0
  22. package/dist/base-skills/notis-spaces/references/context.md +81 -0
  23. package/dist/base-skills/notis-spaces/references/design.md +141 -0
  24. package/dist/base-skills/notis-spaces/references/fixtures.md +29 -0
  25. package/dist/base-skills/notis-spaces/references/memory.md +58 -0
  26. package/dist/base-skills/notis-spaces/references/reading.md +73 -0
  27. package/dist/base-skills/notis-spaces/references/release.md +120 -0
  28. package/dist/base-skills/notis-spaces/references/reports.md +77 -0
  29. package/dist/base-skills/notis-spaces/references/sdk.md +273 -0
  30. package/dist/base-skills/notis-spaces/references/store.md +118 -0
  31. package/dist/base-skills/notis-spaces/references/troubleshooting.md +28 -0
  32. package/dist/base-skills/notis-spaces/references/views.md +229 -0
  33. package/{template/packages → dist}/sdk/package.json +9 -10
  34. package/dist/sdk/src/agentContext.ts +36 -0
  35. package/dist/sdk/src/bundleModule.ts +175 -0
  36. package/dist/sdk/src/components/Dialog.tsx +103 -0
  37. package/dist/sdk/src/components/DocumentEditor.tsx +113 -0
  38. package/dist/sdk/src/components/DocumentPage.tsx +184 -0
  39. package/{template/packages → dist}/sdk/src/components/Markdown.tsx +9 -3
  40. package/{template/packages → dist}/sdk/src/components/MarkdownEditor.tsx +26 -5
  41. package/{template/packages → dist}/sdk/src/components/MultiSelectActionBar.tsx +95 -12
  42. package/{template/packages → dist}/sdk/src/components/MultiSelectCheckbox.tsx +4 -2
  43. package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  44. package/{template/packages → dist}/sdk/src/components/NotisSelectionBoundary.tsx +9 -5
  45. package/dist/sdk/src/components/RecordComponents.tsx +111 -0
  46. package/dist/sdk/src/components/Skeleton.tsx +38 -0
  47. package/dist/sdk/src/config.ts +31 -0
  48. package/{template/packages → dist}/sdk/src/documents.ts +21 -6
  49. package/dist/sdk/src/hooks/useAction.ts +28 -0
  50. package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
  51. package/{template/packages → dist}/sdk/src/hooks/useBackend.ts +3 -2
  52. package/dist/sdk/src/hooks/useCloudComputer.ts +94 -0
  53. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  54. package/{template/packages → dist}/sdk/src/hooks/useCollectionInteractions.ts +157 -147
  55. package/dist/sdk/src/hooks/useDatabaseSchema.ts +50 -0
  56. package/{template/packages → dist}/sdk/src/hooks/useDatabaseSubscription.ts +2 -2
  57. package/dist/sdk/src/hooks/useDocument.ts +51 -0
  58. package/{template/packages → dist}/sdk/src/hooks/useDocuments.ts +25 -59
  59. package/{template/packages → dist}/sdk/src/hooks/useHandover.ts +7 -7
  60. package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
  61. package/dist/sdk/src/hooks/useMarqueeSelection.ts +194 -0
  62. package/{template/packages → dist}/sdk/src/hooks/useMultiSelect.ts +2 -8
  63. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  64. package/dist/sdk/src/hooks/useNativeDocuments.ts +143 -0
  65. package/{template/packages → dist}/sdk/src/hooks/useNotis.ts +6 -1
  66. package/dist/sdk/src/hooks/useNotisNavigation.ts +70 -0
  67. package/dist/sdk/src/hooks/useQuery.ts +81 -0
  68. package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
  69. package/dist/sdk/src/hooks/useTopBarSearch.ts +102 -0
  70. package/dist/sdk/src/hooks/useTopBarTitleLeaf.ts +75 -0
  71. package/{template/packages → dist}/sdk/src/hooks/useUpsertDocument.ts +1 -1
  72. package/dist/sdk/src/hooks/useViewParams.ts +195 -0
  73. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  74. package/{template/packages → dist}/sdk/src/index.ts +64 -1
  75. package/{template/packages → dist}/sdk/src/interactions/actions.ts +14 -1
  76. package/{template/packages → dist}/sdk/src/interactions/shortcuts.tsx +129 -23
  77. package/dist/sdk/src/interactions/visibility.ts +13 -0
  78. package/{template/packages → dist}/sdk/src/interactions.ts +5 -1
  79. package/dist/sdk/src/nativeData.ts +39 -0
  80. package/{template/packages → dist}/sdk/src/provider.tsx +9 -4
  81. package/dist/sdk/src/queryCache.ts +292 -0
  82. package/dist/sdk/src/queryErrors.ts +51 -0
  83. package/dist/sdk/src/runtime.ts +675 -0
  84. package/dist/sdk/src/runtimeModules.ts +33 -0
  85. package/dist/sdk/src/space.ts +442 -0
  86. package/dist/sdk/src/standardViewParams.ts +36 -0
  87. package/{template/packages → dist}/sdk/src/styles.css +81 -16
  88. package/dist/sdk/src/tailwind.ts +66 -0
  89. package/dist/sdk/src/ui/charts/RenderChartData.tsx +25 -0
  90. package/dist/sdk/src/ui/engine/core.ts +392 -0
  91. package/dist/sdk/src/ui/engine/dom.ts +183 -0
  92. package/dist/sdk/src/ui/engine/hooks.ts +195 -0
  93. package/dist/sdk/src/ui/engine/instance.ts +63 -0
  94. package/dist/sdk/src/ui/engine/keycapContext.ts +2 -0
  95. package/dist/sdk/src/ui/engine/registry.ts +262 -0
  96. package/dist/sdk/src/ui/engine/types.ts +62 -0
  97. package/dist/sdk/src/ui/icons.tsx +91 -0
  98. package/dist/sdk/src/ui/util.ts +83 -0
  99. package/dist/sdk/src/viewPeriod.ts +26 -0
  100. package/dist/sdk/src/vite.ts +92 -0
  101. package/dist/sdk/src/words/actions.ts +295 -0
  102. package/dist/sdk/src/words/clashes.ts +158 -0
  103. package/dist/sdk/src/words/format.ts +136 -0
  104. package/dist/sdk/src/words/index.ts +13 -0
  105. package/dist/sdk/src/words/keymap.ts +72 -0
  106. package/dist/sdk/src/words/keys.ts +297 -0
  107. package/dist/sdk/src/words/shell.ts +46 -0
  108. package/dist/sdk/src/words/types.ts +65 -0
  109. package/dist/skill-sync/index.js +832 -151
  110. package/dist/skill-sync/index.js.map +4 -4
  111. package/dist/skill-sync-worker.mjs +3941 -0
  112. package/dist/space-harness/host.js +1460 -0
  113. package/dist/space-sdk/dist/chunk-ZSN57ATH.js +5 -0
  114. package/dist/space-sdk/dist/index.js +15 -0
  115. package/dist/space-sdk/dist/ui.js +18 -0
  116. package/dist/space-sdk/package.json +33 -0
  117. package/dist/space-sdk/src/agentContext.d.ts +36 -0
  118. package/dist/space-sdk/src/components/Dialog.d.ts +20 -0
  119. package/dist/space-sdk/src/components/DocumentEditor.d.ts +46 -0
  120. package/dist/space-sdk/src/components/DocumentPage.d.ts +53 -0
  121. package/dist/space-sdk/src/components/Markdown.d.ts +18 -0
  122. package/dist/space-sdk/src/components/MarkdownEditor.d.ts +11 -0
  123. package/dist/space-sdk/src/components/MultiSelectActionBar.d.ts +31 -0
  124. package/dist/space-sdk/src/components/MultiSelectCheckbox.d.ts +14 -0
  125. package/dist/space-sdk/src/components/MultiSelectDragOverlay.d.ts +8 -0
  126. package/dist/space-sdk/src/components/NotisCommentBoundary.d.ts +36 -0
  127. package/dist/space-sdk/src/components/NotisSelectionBoundary.d.ts +16 -0
  128. package/dist/space-sdk/src/components/RecordComponents.d.ts +64 -0
  129. package/dist/space-sdk/src/components/ShortcutHints.d.ts +12 -0
  130. package/dist/space-sdk/src/components/Skeleton.d.ts +23 -0
  131. package/dist/space-sdk/src/config.ts +31 -0
  132. package/dist/space-sdk/src/documents.d.ts +59 -0
  133. package/dist/space-sdk/src/hooks/useAction.d.ts +8 -0
  134. package/dist/space-sdk/src/hooks/useActiveResource.d.ts +3 -0
  135. package/dist/space-sdk/src/hooks/useAgentContext.d.ts +7 -0
  136. package/dist/space-sdk/src/hooks/useBackend.d.ts +23 -0
  137. package/dist/space-sdk/src/hooks/useCloudComputer.d.ts +45 -0
  138. package/dist/space-sdk/src/hooks/useCloudComputerShell.d.ts +49 -0
  139. package/dist/space-sdk/src/hooks/useCollectionInteractions.d.ts +118 -0
  140. package/dist/space-sdk/src/hooks/useDatabaseSchema.d.ts +14 -0
  141. package/dist/space-sdk/src/hooks/useDatabaseSubscription.d.ts +26 -0
  142. package/dist/space-sdk/src/hooks/useDocument.d.ts +22 -0
  143. package/dist/space-sdk/src/hooks/useDocuments.d.ts +31 -0
  144. package/dist/space-sdk/src/hooks/useHandover.d.ts +43 -0
  145. package/dist/space-sdk/src/hooks/useLongPressSelection.d.ts +9 -0
  146. package/dist/space-sdk/src/hooks/useMarqueeSelection.d.ts +57 -0
  147. package/dist/space-sdk/src/hooks/useMultiSelect.d.ts +51 -0
  148. package/dist/space-sdk/src/hooks/useNativeDocumentBody.d.ts +24 -0
  149. package/dist/space-sdk/src/hooks/useNativeDocuments.d.ts +55 -0
  150. package/dist/space-sdk/src/hooks/useNotis.d.ts +25 -0
  151. package/dist/space-sdk/src/hooks/useNotisNavigation.d.ts +33 -0
  152. package/dist/space-sdk/src/hooks/useQuery.d.ts +22 -0
  153. package/dist/space-sdk/src/hooks/useTool.d.ts +20 -0
  154. package/dist/space-sdk/src/hooks/useToolQuery.d.ts +3 -0
  155. package/dist/space-sdk/src/hooks/useTools.d.ts +15 -0
  156. package/dist/space-sdk/src/hooks/useTopBarSearch.d.ts +48 -0
  157. package/dist/space-sdk/src/hooks/useTopBarTitleLeaf.d.ts +24 -0
  158. package/dist/space-sdk/src/hooks/useUpsertDocument.d.ts +27 -0
  159. package/dist/space-sdk/src/hooks/useViewParams.d.ts +76 -0
  160. package/dist/space-sdk/src/hooks/useViewerReads.d.ts +38 -0
  161. package/dist/space-sdk/src/index.d.ts +98 -0
  162. package/dist/space-sdk/src/interactions/actions.d.ts +33 -0
  163. package/dist/space-sdk/src/interactions/shortcuts.d.ts +42 -0
  164. package/dist/space-sdk/src/interactions/visibility.d.ts +2 -0
  165. package/dist/space-sdk/src/nativeData.d.ts +66 -0
  166. package/dist/space-sdk/src/provider.d.ts +17 -0
  167. package/dist/space-sdk/src/queryCache.d.ts +45 -0
  168. package/dist/space-sdk/src/queryErrors.ts +51 -0
  169. package/dist/space-sdk/src/runtime.d.ts +615 -0
  170. package/dist/space-sdk/src/runtimeModules.ts +33 -0
  171. package/dist/space-sdk/src/space.ts +442 -0
  172. package/dist/space-sdk/src/standardViewParams.d.ts +15 -0
  173. package/dist/space-sdk/src/tailwind-candidates.d.ts +3418 -0
  174. package/dist/space-sdk/src/tailwind.ts +66 -0
  175. package/dist/space-sdk/src/ui/actions/ActionToast.d.ts +15 -0
  176. package/dist/space-sdk/src/ui/actions/actionWords.d.ts +16 -0
  177. package/dist/space-sdk/src/ui/actions/index.d.ts +7 -0
  178. package/dist/space-sdk/src/ui/actions/useActions.d.ts +69 -0
  179. package/dist/space-sdk/src/ui/actions/useToast.d.ts +41 -0
  180. package/dist/space-sdk/src/ui/blocks/Tabs.d.ts +139 -0
  181. package/dist/space-sdk/src/ui/blocks/blocks.d.ts +217 -0
  182. package/dist/space-sdk/src/ui/blocks/index.d.ts +4 -0
  183. package/dist/space-sdk/src/ui/board/BoardView.d.ts +36 -0
  184. package/dist/space-sdk/src/ui/board/boardModel.d.ts +46 -0
  185. package/dist/space-sdk/src/ui/board/boardWords.d.ts +9 -0
  186. package/dist/space-sdk/src/ui/board/index.d.ts +5 -0
  187. package/dist/space-sdk/src/ui/calendar/CalendarView.d.ts +49 -0
  188. package/dist/space-sdk/src/ui/calendar/DayPicker.d.ts +23 -0
  189. package/dist/space-sdk/src/ui/calendar/calendarMath.d.ts +81 -0
  190. package/dist/space-sdk/src/ui/charts/Chart.d.ts +203 -0
  191. package/dist/space-sdk/src/ui/charts/ChartTable.d.ts +37 -0
  192. package/dist/space-sdk/src/ui/charts/RenderChartData.d.ts +16 -0
  193. package/dist/space-sdk/src/ui/charts/StatRow.d.ts +59 -0
  194. package/dist/space-sdk/src/ui/charts/chartData.d.ts +84 -0
  195. package/dist/space-sdk/src/ui/comments/CommentPane.d.ts +58 -0
  196. package/dist/space-sdk/src/ui/comments/commentModel.d.ts +157 -0
  197. package/dist/space-sdk/src/ui/comments/commentWords.d.ts +60 -0
  198. package/dist/space-sdk/src/ui/comments/index.d.ts +4 -0
  199. package/dist/space-sdk/src/ui/comments/useComments.d.ts +146 -0
  200. package/dist/space-sdk/src/ui/engine/HelpSheet.d.ts +12 -0
  201. package/dist/space-sdk/src/ui/engine/KeyboardEngine.d.ts +27 -0
  202. package/dist/space-sdk/src/ui/engine/core.d.ts +73 -0
  203. package/dist/space-sdk/src/ui/engine/hooks.d.ts +64 -0
  204. package/dist/space-sdk/src/ui/engine/index.d.ts +14 -0
  205. package/dist/space-sdk/src/ui/engine/instance.d.ts +21 -0
  206. package/dist/space-sdk/src/ui/engine/keycapContext.d.ts +2 -0
  207. package/dist/space-sdk/src/ui/engine/registry.d.ts +77 -0
  208. package/dist/space-sdk/src/ui/engine/step.d.ts +11 -0
  209. package/dist/space-sdk/src/ui/engine/types.d.ts +54 -0
  210. package/dist/space-sdk/src/ui/fields/fieldWords.d.ts +56 -0
  211. package/dist/space-sdk/src/ui/fields/fields.d.ts +162 -0
  212. package/dist/space-sdk/src/ui/fields/index.d.ts +5 -0
  213. package/dist/space-sdk/src/ui/fields/useFieldDraft.d.ts +22 -0
  214. package/dist/space-sdk/src/ui/gallery/AttachmentGallery.d.ts +53 -0
  215. package/dist/space-sdk/src/ui/gallery/ImageViewer.d.ts +27 -0
  216. package/dist/space-sdk/src/ui/gallery/galleryWords.d.ts +95 -0
  217. package/dist/space-sdk/src/ui/gallery/index.d.ts +5 -0
  218. package/dist/space-sdk/src/ui/icons.d.ts +13 -0
  219. package/dist/space-sdk/src/ui/index.d.ts +54 -0
  220. package/dist/space-sdk/src/ui/keycap/Keycap.d.ts +22 -0
  221. package/dist/space-sdk/src/ui/keycap/index.d.ts +2 -0
  222. package/dist/space-sdk/src/ui/layout/Panels.d.ts +50 -0
  223. package/dist/space-sdk/src/ui/layout/chatPanelEdge.d.ts +77 -0
  224. package/dist/space-sdk/src/ui/layout/index.d.ts +5 -0
  225. package/dist/space-sdk/src/ui/layout/panelMath.d.ts +91 -0
  226. package/dist/space-sdk/src/ui/list/ListReader.d.ts +380 -0
  227. package/dist/space-sdk/src/ui/list/Row.d.ts +145 -0
  228. package/dist/space-sdk/src/ui/list/SelectionBar.d.ts +65 -0
  229. package/dist/space-sdk/src/ui/list/index.d.ts +14 -0
  230. package/dist/space-sdk/src/ui/list/rowState.d.ts +22 -0
  231. package/dist/space-sdk/src/ui/list/useListKeys.d.ts +89 -0
  232. package/dist/space-sdk/src/ui/list/useReorder.d.ts +86 -0
  233. package/dist/space-sdk/src/ui/list/useSelection.d.ts +90 -0
  234. package/dist/space-sdk/src/ui/open/OpenAsMenu.d.ts +15 -0
  235. package/dist/space-sdk/src/ui/open/index.d.ts +6 -0
  236. package/dist/space-sdk/src/ui/open/openModeStore.d.ts +39 -0
  237. package/dist/space-sdk/src/ui/open/useOpenMode.d.ts +17 -0
  238. package/dist/space-sdk/src/ui/overlay/Menu.d.ts +88 -0
  239. package/dist/space-sdk/src/ui/overlay/Popover.d.ts +28 -0
  240. package/dist/space-sdk/src/ui/overlay/Tooltip.d.ts +19 -0
  241. package/dist/space-sdk/src/ui/overlay/index.d.ts +9 -0
  242. package/dist/space-sdk/src/ui/overlay/layerRoot.d.ts +24 -0
  243. package/dist/space-sdk/src/ui/overlay/position.d.ts +31 -0
  244. package/dist/space-sdk/src/ui/period/PeriodPicker.d.ts +30 -0
  245. package/dist/space-sdk/src/ui/period/dateFormat.d.ts +50 -0
  246. package/dist/space-sdk/src/ui/period/dateWords.d.ts +88 -0
  247. package/dist/space-sdk/src/ui/period/periods.d.ts +70 -0
  248. package/dist/space-sdk/src/ui/period/usePeriod.d.ts +23 -0
  249. package/dist/space-sdk/src/ui/present/index.d.ts +3 -0
  250. package/dist/space-sdk/src/ui/present/presentWords.d.ts +27 -0
  251. package/dist/space-sdk/src/ui/present/screens.d.ts +36 -0
  252. package/dist/space-sdk/src/ui/present/useAudienceScreen.d.ts +38 -0
  253. package/dist/space-sdk/src/ui/styles.d.ts +21 -0
  254. package/dist/space-sdk/src/ui/timeline/TimelineView.d.ts +44 -0
  255. package/dist/space-sdk/src/ui/timeline/timelineMath.d.ts +123 -0
  256. package/dist/space-sdk/src/viewPeriod.d.ts +8 -0
  257. package/dist/space-sdk/src/vite.ts +92 -0
  258. package/dist/space-sdk/src/words/actions.ts +295 -0
  259. package/dist/space-sdk/src/words/clashes.ts +158 -0
  260. package/dist/space-sdk/src/words/format.ts +136 -0
  261. package/dist/space-sdk/src/words/index.ts +13 -0
  262. package/dist/space-sdk/src/words/keymap.ts +72 -0
  263. package/dist/space-sdk/src/words/keys.ts +297 -0
  264. package/dist/space-sdk/src/words/shell.ts +46 -0
  265. package/dist/space-sdk/src/words/types.ts +65 -0
  266. package/dist/view-renderer/dist/host.css +1 -0
  267. package/dist/view-renderer/dist/host.js +2374 -0
  268. package/dist/view-renderer/dist/theme.css +73 -0
  269. package/dist/view-renderer/src/dom.js +212 -0
  270. package/dist/view-renderer/src/errors.js +8 -0
  271. package/dist/view-renderer/src/host.tsx +250 -0
  272. package/dist/view-renderer/src/index.js +4 -0
  273. package/dist/view-renderer/src/png.js +213 -0
  274. package/dist/view-renderer/src/pool.js +110 -0
  275. package/dist/view-renderer/src/remote-broker.js +59 -0
  276. package/dist/view-renderer/src/renderer.js +279 -0
  277. package/package.json +21 -11
  278. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +63 -12
  279. package/skills/notis-onboarding/BRIEF.md +14 -9
  280. package/skills/notis-query/cli.md +1 -1
  281. package/skills/notis-spaces/cli.md +540 -0
  282. package/src/cli.js +8 -8
  283. package/src/command-specs/agents.js +121 -8
  284. package/src/command-specs/auth.js +3 -3
  285. package/src/command-specs/diagnostics.js +97 -20
  286. package/src/command-specs/documents.js +158 -0
  287. package/src/command-specs/files.js +29 -0
  288. package/src/command-specs/handover.js +331 -79
  289. package/src/command-specs/helpers.js +50 -40
  290. package/src/command-specs/index.js +14 -5
  291. package/src/command-specs/meta.js +38 -9
  292. package/src/command-specs/onboarding.js +1 -1
  293. package/src/command-specs/profile.js +42 -1
  294. package/src/command-specs/skills.js +314 -9
  295. package/src/command-specs/smoke.js +16 -4
  296. package/src/command-specs/spaces.js +783 -0
  297. package/src/command-specs/tools.js +118 -21
  298. package/src/command-specs/version-history.js +20 -0
  299. package/src/command-specs/views.js +69 -0
  300. package/src/runtime/agent-browser.js +178 -267
  301. package/src/runtime/agent-setup.js +172 -13
  302. package/src/runtime/app-boundary-validator.js +338 -2
  303. package/src/runtime/app-data-literals.js +82 -0
  304. package/src/runtime/app-platform.js +750 -1570
  305. package/src/runtime/auth-recovery.js +17 -0
  306. package/src/runtime/base-skills.d.ts +11 -0
  307. package/src/runtime/base-skills.js +100 -3
  308. package/src/runtime/computer-identity.d.ts +30 -0
  309. package/src/runtime/computer-identity.js +151 -0
  310. package/src/runtime/config-lock.js +135 -0
  311. package/src/runtime/confined-space-build.js +74 -0
  312. package/src/runtime/confined-space-verify.js +49 -0
  313. package/src/runtime/delegated-context.js +5 -21
  314. package/src/runtime/native-record-files.js +47 -0
  315. package/src/runtime/oauth.js +342 -31
  316. package/src/runtime/profiles.js +19 -112
  317. package/src/runtime/retired-sdk-files.js +24 -0
  318. package/src/runtime/retry.js +150 -0
  319. package/src/runtime/skill-sync/cloud-client.ts +33 -8
  320. package/src/runtime/skill-sync/index.ts +443 -106
  321. package/src/runtime/skill-sync/local-scanner.ts +248 -32
  322. package/src/runtime/skill-sync/native-identity.ts +102 -0
  323. package/src/runtime/skill-sync/native-plan.ts +70 -0
  324. package/src/runtime/skill-sync/symlink-manager.ts +66 -16
  325. package/src/runtime/skill-sync/sync-plan.ts +34 -2
  326. package/src/runtime/skill-sync/types.ts +54 -0
  327. package/src/runtime/skill-sync/write-cloud-skill.ts +4 -4
  328. package/src/runtime/skill-sync-service.js +109 -0
  329. package/src/runtime/source-root.js +32 -0
  330. package/src/runtime/space-collection-scaffold.js +107 -0
  331. package/src/runtime/space-database-structures.js +229 -0
  332. package/src/runtime/space-fixture-broker.js +68 -0
  333. package/src/runtime/space-lock.js +337 -0
  334. package/src/runtime/space-platform.js +133 -0
  335. package/src/runtime/space-source.js +290 -0
  336. package/src/runtime/space-test-server.js +354 -0
  337. package/src/runtime/space-verification.js +94 -0
  338. package/src/runtime/space-verify-browser-worker.js +51 -0
  339. package/src/runtime/space-view-manifest.js +368 -0
  340. package/src/runtime/sync-skills.d.ts +5 -1
  341. package/src/runtime/sync-skills.js +11 -2
  342. package/src/runtime/tool-confirmation.js +138 -0
  343. package/src/runtime/transport.js +118 -24
  344. package/src/runtime/upsell.js +219 -0
  345. package/src/runtime/view-render.js +49 -0
  346. package/src/skill-sync-worker-entry.js +2 -0
  347. package/src/skill-sync-worker.js +114 -0
  348. package/src/space-harness-host.ts +122 -0
  349. package/template/.harness/index.html.tmpl +9 -352
  350. package/template/app/globals.css +29 -3
  351. package/template/app/layout.tsx +2 -6
  352. package/template/components/ui/badge.tsx +7 -4
  353. package/template/components/ui/button.tsx +1 -1
  354. package/template/components/ui/native-select.tsx +24 -0
  355. package/template/notis.config.ts +4 -36
  356. package/template/package-lock.json +1800 -1867
  357. package/template/package.json +16 -14
  358. package/template/postcss.config.mjs +1 -1
  359. package/template/tailwind.config.ts +1 -6
  360. package/template/tsconfig.json +1 -0
  361. package/template/vite.config.ts +2 -2
  362. package/dist/base-skills/notis-apps/SKILL.md +0 -613
  363. package/skills/notis-apps/cli.md +0 -285
  364. package/src/command-specs/apps.js +0 -3011
  365. package/src/runtime/app-changelog.js +0 -79
  366. package/src/runtime/app-dev-consumers.js +0 -154
  367. package/src/runtime/app-dev-host-lock.js +0 -80
  368. package/src/runtime/app-dev-process-identity.js +0 -111
  369. package/src/runtime/app-dev-roots.js +0 -284
  370. package/src/runtime/app-dev-server.js +0 -1140
  371. package/src/runtime/app-dev-sessions.js +0 -185
  372. package/src/runtime/app-registry-scaffolds.js +0 -367
  373. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  374. package/src/runtime/cli-mode.generated.js +0 -5
  375. package/src/runtime/cli-mode.js +0 -34
  376. package/src/runtime/store-screenshot.js +0 -143
  377. package/template/CHANGELOG.md +0 -5
  378. package/template/app/page.tsx +0 -60
  379. package/template/components/ui/card.tsx +0 -56
  380. package/template/packages/sdk/src/components/DocumentEditor.tsx +0 -103
  381. package/template/packages/sdk/src/config.ts +0 -259
  382. package/template/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  383. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  384. package/template/packages/sdk/src/hooks/useDocument.ts +0 -78
  385. package/template/packages/sdk/src/hooks/useNotisNavigation.ts +0 -52
  386. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  387. package/template/packages/sdk/src/runtime.ts +0 -439
  388. package/template/packages/sdk/src/vite.ts +0 -56
  389. package/template/packages/sdk/tsconfig.json +0 -15
  390. /package/{template/packages → dist}/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
  391. /package/{template/packages → dist}/sdk/src/components/ShortcutHints.tsx +0 -0
  392. /package/{template/packages → dist}/sdk/src/hooks/useActiveResource.ts +0 -0
  393. /package/{template/packages → dist}/sdk/src/hooks/useTool.ts +0 -0
  394. /package/{template/packages → dist}/sdk/src/hooks/useTools.ts +0 -0
  395. /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
- * This is the polite half of the guard. The server refuses the same call unless
9
- * it carries a purpose-scoped CLI/MCP OAuth token, which a delegated runtime
10
- * never has -- that is the half that actually holds. What this adds is a clear
11
- * message at the point of the mistake, instead of a permission error the agent
12
- * will try to work around.
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
+ }
@@ -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 { getAuthRecovery, quoteShellArgument } from './auth-recovery.js';
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:apps',
52
+ 'notis:spaces',
46
53
  ];
47
- const OAUTH_LOCK_DIR = join(homedir(), '.notis', 'oauth.lock');
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
- function buildAuthorizeUrl(metadata, {
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
- : DEFAULT_CLI_OAUTH_SCOPES;
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(OAUTH_LOCK_DIR).mtimeMs > 45_000;
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 = 60_000) {
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(OAUTH_LOCK_DIR);
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(OAUTH_LOCK_DIR);
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
- export async function refreshOAuthCredential(runtime, fetchImpl = fetch) {
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
- rotatedResponse = await fetchJson(
2467
- metadata.tokenEndpoint,
2468
- {
2469
- method: 'POST',
2470
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
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
- fetchImpl,
2479
- );
2480
- const updated = persistOAuthTokenResponse(runtime, metadata, rotatedResponse);
2481
- rotatedPersisted = true;
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
- hints: getAuthRecovery(runtime).hints,
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(OAUTH_LOCK_DIR);
2815
+ rmdirSync(oauthLockDir());
2505
2816
  } catch {
2506
2817
  // A process exit or external cleanup may already have removed the lock.
2507
2818
  }