@notis_ai/cli 0.2.17 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +42323 -18258
  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 +271 -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 -1568
  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 -3000
  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
@@ -8,7 +8,7 @@ import {
8
8
  NOTIS_SKILL_SYNC_ROOT,
9
9
  safeName,
10
10
  } from './local-scanner';
11
- import { normalizeAgentTargets, type CloudSkill, type NotisSyncState } from './types';
11
+ import { normalizeAgentTargets, type AgentTargets, type SkillSyncFailure, type CloudSkill, type NotisSyncState } from './types';
12
12
 
13
13
  const HOME_DIR = os.homedir();
14
14
 
@@ -22,6 +22,22 @@ type ExternalAgent = keyof typeof EXTERNAL_AGENT_SKILL_DIRS;
22
22
 
23
23
  const EXTERNAL_AGENTS = Object.keys(EXTERNAL_AGENT_SKILL_DIRS) as ExternalAgent[];
24
24
 
25
+ const AGENT_FAILURE_LABELS: Record<string, string> = {
26
+ claude_code: 'Claude Code',
27
+ cursor: 'Cursor',
28
+ codex: 'Codex',
29
+ legacy: 'legacy ~/.agents/skills',
30
+ };
31
+
32
+ /** Failures are shown next to skill names in the Portal and CLI; never leak raw agent ids. */
33
+ function agentFailureLabel(agent: string): string {
34
+ return AGENT_FAILURE_LABELS[agent] ?? agent;
35
+ }
36
+
37
+ function agentFolderFailureName(agent: string): string {
38
+ return `${agentFailureLabel(agent)} skills folder`;
39
+ }
40
+
25
41
  export interface DeletedAgentSymlink {
26
42
  skillId: string;
27
43
  skillName: string;
@@ -46,6 +62,8 @@ export interface SymlinkSyncResult {
46
62
  linked: number;
47
63
  removed: number;
48
64
  skipped: number;
65
+ verifiedAgentLinks?: Record<string, Partial<AgentTargets>>;
66
+ failures?: SkillSyncFailure[];
49
67
  }
50
68
 
51
69
  /** Remove only links into another account's managed mirror. This is safe even
@@ -128,8 +146,9 @@ async function isManagedSymlink(linkPath: string, managedRoots: string[]): Promi
128
146
  return managedRoots.some((root) => (
129
147
  resolvedTarget === root || resolvedTarget.startsWith(`${root}${path.sep}`)
130
148
  ));
131
- } catch {
132
- return false;
149
+ } catch (error) {
150
+ if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return false;
151
+ throw error;
133
152
  }
134
153
  }
135
154
 
@@ -146,8 +165,8 @@ async function ensureCorrectSymlink(linkPath: string, targetPath: string): Promi
146
165
  } else {
147
166
  return 'blocked';
148
167
  }
149
- } catch {
150
- // Missing path is expected.
168
+ } catch (error) {
169
+ if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') throw error;
151
170
  }
152
171
 
153
172
  const relativePath = path.relative(path.dirname(linkPath), targetPath);
@@ -166,6 +185,8 @@ async function removeUndesiredManagedSymlinks(
166
185
  agentDir: string,
167
186
  desiredSkills: Set<string>,
168
187
  managedRoots: string[],
188
+ failures: SkillSyncFailure[],
189
+ agent: string,
169
190
  ): Promise<number> {
170
191
  let removed = 0;
171
192
  try {
@@ -173,13 +194,19 @@ async function removeUndesiredManagedSymlinks(
173
194
  const existingEntries = await fs.readdir(agentDir, { withFileTypes: true });
174
195
  for (const entry of existingEntries) {
175
196
  const entryPath = path.join(agentDir, entry.name);
176
- if (!desiredSkills.has(entry.name) && await isManagedSymlink(entryPath, managedRoots)) {
177
- await fs.unlink(entryPath);
178
- removed += 1;
197
+ try {
198
+ if (!desiredSkills.has(entry.name) && await isManagedSymlink(entryPath, managedRoots)) {
199
+ await fs.unlink(entryPath);
200
+ removed += 1;
201
+ }
202
+ } catch (error) {
203
+ if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
204
+ failures.push({ name: entry.name, error: `${agentFailureLabel(agent)}: could not remove skill link (${(error as Error).message})` });
205
+ }
179
206
  }
180
207
  }
181
- } catch {
182
- // Missing or unreadable agent directories should not block skill sync.
208
+ } catch (error) {
209
+ failures.push({ name: agentFolderFailureName(agent), error: `Could not read agent skills directory (${(error as Error).message})` });
183
210
  }
184
211
  return removed;
185
212
  }
@@ -255,7 +282,9 @@ export async function detectDeletedAgentSymlinks(
255
282
  continue;
256
283
  }
257
284
  const previous = previousState.skills[skill.name];
258
- if (!previous) {
285
+ if (!previous || previous.cloudId !== skill.id
286
+ || previous.verifiedAgentLinks?.[agent] !== true
287
+ || !skill.updated_at || previous.cloudUpdatedAt !== skill.updated_at) {
259
288
  continue;
260
289
  }
261
290
 
@@ -293,10 +322,12 @@ export async function syncSymlinks(
293
322
  ): Promise<SymlinkSyncResult> {
294
323
  await fs.mkdir(skillsDir, { recursive: true });
295
324
 
296
- const result: SymlinkSyncResult = {
325
+ const result: Required<SymlinkSyncResult> = {
297
326
  linked: 0,
298
327
  removed: 0,
299
328
  skipped: 0,
329
+ verifiedAgentLinks: {},
330
+ failures: [],
300
331
  };
301
332
  const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
302
333
  const legacyGlobalSkillsDir = options.legacyGlobalSkillsDir || LEGACY_AGENTS_SKILLS_DIR;
@@ -336,23 +367,36 @@ export async function syncSymlinks(
336
367
  continue;
337
368
  }
338
369
 
339
- await fs.mkdir(agentDir, { recursive: true });
370
+ try {
371
+ await fs.mkdir(agentDir, { recursive: true });
372
+ } catch (error) {
373
+ result.failures.push({ name: agentFolderFailureName(agent), error: `Could not create agent skills directory (${(error as Error).message})` });
374
+ continue;
375
+ }
340
376
 
341
377
  const desiredSkills = desiredByAgent[agent];
342
378
  if (options.removeUndesired !== false) {
343
- result.removed += await removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots);
379
+ result.removed += await removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots, result.failures, agent);
344
380
  }
345
381
 
346
382
  for (const skillName of desiredSkills) {
347
383
  const targetPath = path.join(skillsDir, skillName);
348
384
  const linkPath = path.join(agentDir, safeName(skillName, agentDir));
349
385
  try {
350
- await fs.access(targetPath);
386
+ if (!(await fs.stat(path.join(targetPath, "SKILL.md"))).isFile()) throw new Error("Missing SKILL.md");
351
387
  } catch {
352
388
  result.skipped += 1;
389
+ result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: SKILL.md is missing or unreadable` });
390
+ continue;
391
+ }
392
+ let syncOutcome;
393
+ try {
394
+ syncOutcome = await ensureCorrectSymlink(linkPath, targetPath);
395
+ } catch (error) {
396
+ result.skipped += 1;
397
+ result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: could not create skill link (${(error as Error).message})` });
353
398
  continue;
354
399
  }
355
- const syncOutcome = await ensureCorrectSymlink(linkPath, targetPath);
356
400
  if (syncOutcome === 'linked') {
357
401
  result.linked += 1;
358
402
  } else if (syncOutcome === 'blocked') {
@@ -360,9 +404,13 @@ export async function syncSymlinks(
360
404
  `[skill-sync] Could not link "${skillName}" for ${agent}: non-symlink entry blocks ${linkPath}`,
361
405
  );
362
406
  result.skipped += 1;
407
+ result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: an existing file or folder blocks the skill link` });
363
408
  } else {
364
409
  result.skipped += 1;
365
410
  }
411
+ if (syncOutcome !== "blocked") {
412
+ result.verifiedAgentLinks[skillName] = { ...result.verifiedAgentLinks[skillName], [agent]: true };
413
+ }
366
414
  }
367
415
  }
368
416
 
@@ -376,6 +424,8 @@ export async function syncSymlinks(
376
424
  legacyGlobalSkillsDir,
377
425
  new Set(),
378
426
  managedRoots,
427
+ result.failures,
428
+ 'legacy',
379
429
  );
380
430
  }
381
431
 
@@ -1,22 +1,54 @@
1
- import type { LocalSkill, NotisSyncState } from "./types";
1
+ import type { CloudSkill, LocalSkill, NotisSyncState } from "./types";
2
+ import { isNativeSkill } from './native-identity';
3
+ import { hasNativeFolderHistory } from './native-plan';
4
+
5
+ /**
6
+ * One cloud row per local folder. Several active rows can carry the same skill name
7
+ * (duplicates created by earlier pushes), and every one of them writes to the same
8
+ * directory. Applying them all in
9
+ * one pass replaced that directory several times per sync and left whichever row came
10
+ * last on disk. The newest revision wins, deterministically.
11
+ */
12
+ export function selectCloudSkillsToApply(skills: CloudSkill[]): CloudSkill[] {
13
+ const winners = new Map<string, CloudSkill>();
14
+ for (const skill of skills) {
15
+ const current = winners.get(skill.name);
16
+ if (!current || (skill.updated_at || "") >= (current.updated_at || "")) {
17
+ winners.set(skill.name, skill);
18
+ }
19
+ }
20
+ return [...winners.values()];
21
+ }
2
22
 
3
23
  export function getPushCandidates(
4
24
  localSkills: LocalSkill[],
5
25
  syncState: NotisSyncState,
6
26
  cloudCuratedSkillNames: ReadonlySet<string> = new Set(),
7
27
  cloudSkillNames?: ReadonlySet<string>,
28
+ cloudSkills: CloudSkill[] = [],
8
29
  ): LocalSkill[] {
9
30
  return localSkills.filter((skill) => {
10
31
  if (cloudCuratedSkillNames.has(skill.name)) {
11
32
  return false;
12
33
  }
13
- const previous = syncState.skills[skill.name];
34
+ const previous = syncState.skills[skill.name]
35
+ || (skill.cloudId ? Object.values(syncState.skills).find(row => row.cloudId === skill.cloudId) : undefined);
36
+ const native = cloudSkills.find(row => isNativeSkill(row) && row.id === (skill.cloudId || previous?.cloudId));
37
+ if (native || skill.nativeReference || previous?.nativeReference || previous?.nativeUnavailable) {
38
+ return Boolean(native && skill.nativeReference && skill.nativeScope === native.native_scope
39
+ && (previous ? previous.folderHash !== skill.folderHash : skill.nativeBaseFolderHash !== skill.folderHash));
40
+ }
14
41
  if (!previous) {
42
+ if (hasNativeFolderHistory(syncState, skill.name) || /^native-[a-f0-9-]{36}$/.test(skill.name)) return false;
15
43
  return true;
16
44
  }
17
45
  if (cloudSkillNames && !cloudSkillNames.has(skill.name)) {
18
46
  return false;
19
47
  }
20
48
  return previous.folderHash !== skill.folderHash;
49
+ }).map(skill => {
50
+ const previous = syncState.skills[skill.name]
51
+ || Object.values(syncState.skills).find(row => row.cloudId === skill.cloudId);
52
+ return { ...skill, ...(skill.cloudId || previous?.cloudId ? { cloudId: skill.cloudId || previous!.cloudId } : {}) };
21
53
  });
22
54
  }
@@ -5,10 +5,37 @@ export interface AgentTargets {
5
5
  codex: boolean;
6
6
  }
7
7
 
8
+ export interface NativeSkillReference {
9
+ kind: 'native_standalone_skill';
10
+ skill_id: string;
11
+ access_revision: number;
12
+ version: { revision: number; digest: string };
13
+ }
14
+
8
15
  export interface SyncedSkill {
9
16
  cloudId: string;
17
+ nativeReference?: NativeSkillReference;
18
+ nativeDisplayName?: string;
19
+ nativeScope?: string;
20
+ nativeSettingsRevision?: number;
21
+ /** Retain managed provenance even when its cloud access is removed. */
22
+ nativeUnavailable?: boolean;
10
23
  folderHash: string;
11
24
  agentTargets: AgentTargets;
25
+ /** Actual readable managed links, never inferred from desired targets. */
26
+ verifiedAgentLinks?: Partial<AgentTargets>;
27
+ cloudUpdatedAt?: string;
28
+ /** Content accepted on disk; independent of server-specific folder hash formats. */
29
+ cloudContentHash?: string;
30
+ /** Relative paths the last successful write put in this folder. Anything else in
31
+ * the folder was created locally (a running skill's output) and survives the next
32
+ * cloud write; anything listed here that the cloud later drops is deleted. */
33
+ appliedFiles?: string[];
34
+ /** `skill_folder_hash` of the cloud revision a successful write actually applied.
35
+ * The server may compute that hash with a different construction than the local
36
+ * folder hash (bundle-entry hashes do), so only cloud-to-cloud comparison tells
37
+ * "the cloud moved" apart from "the two sides hash differently". */
38
+ appliedCloudFolderHash?: string;
12
39
  syncedAt: string;
13
40
  }
14
41
 
@@ -16,6 +43,15 @@ export interface NotisSyncState {
16
43
  version: 1;
17
44
  lastSyncedAt: string | null;
18
45
  skills: Record<string, SyncedSkill>;
46
+ /** Historical folder aliases are provenance, never authority or write pins. */
47
+ nativeHistory?: Record<string, { names: string[] }>;
48
+ }
49
+
50
+ /** A successful write, plus the paths it put on disk so the next one can tell
51
+ * cloud-managed files apart from whatever ran inside the folder since. */
52
+ export interface SkillWriteOutcome {
53
+ written: true;
54
+ appliedFiles: string[];
19
55
  }
20
56
 
21
57
  export interface LocalSkill {
@@ -26,6 +62,12 @@ export interface LocalSkill {
26
62
  directoryPath: string;
27
63
  sourceUrl?: string;
28
64
  bundleFiles?: BundleFile[];
65
+ cloudId?: string;
66
+ nativeReference?: NativeSkillReference;
67
+ displayName?: string;
68
+ nativeScope?: string;
69
+ nativeFilesCurrent?: boolean;
70
+ nativeBaseFolderHash?: string;
29
71
  }
30
72
 
31
73
  export interface BundleFile {
@@ -34,6 +76,7 @@ export interface BundleFile {
34
76
  }
35
77
 
36
78
  export interface CloudSkill {
79
+ updated_at?: string;
37
80
  id: string;
38
81
  name: string;
39
82
  description: string | null;
@@ -45,9 +88,17 @@ export interface CloudSkill {
45
88
  bundle_hydration_failed?: boolean | null;
46
89
  source: string;
47
90
  status: string;
91
+ native_reference?: NativeSkillReference;
92
+ display_name?: string;
93
+ native_scope?: string;
94
+ native_settings?: { revision: number; enabled: boolean; agent_targets: AgentTargets; updated_at: string };
48
95
  }
49
96
 
50
97
  export interface SyncSettings {
98
+ skill_sync_protocol?: number;
99
+ sync_scope?: string;
100
+ sync_namespace?: string | null;
101
+ agent_targets_conditional_updates?: boolean;
51
102
  /** Server-verified canonical Notis account id used to scope local mirrors. */
52
103
  user_id?: string;
53
104
  sync_enabled: boolean;
@@ -66,6 +117,9 @@ export interface EntitlementAccessDetail {
66
117
  }
67
118
 
68
119
  export interface SyncPullResponse {
120
+ skill_sync_protocol?: number;
121
+ sync_scope?: string;
122
+ sync_namespace?: string | null;
69
123
  skills: CloudSkill[];
70
124
  sync_enabled: boolean;
71
125
  last_synced_at: string | null;
@@ -1,21 +1,21 @@
1
- import type { CloudSkill } from './types';
1
+ import type { CloudSkill, SkillWriteOutcome } from './types';
2
2
 
3
3
  interface WriteCloudSkillDependencies {
4
4
  downloadSkillBundle: (bundleUrl: string) => Promise<Buffer>;
5
- writeCloudSkillToDisk: (skill: CloudSkill, bundleBytes?: Buffer) => Promise<boolean>;
5
+ writeCloudSkillToDisk: (skill: CloudSkill, bundleBytes?: Buffer) => Promise<SkillWriteOutcome | boolean>;
6
6
  onWarning?: (message: string, error: unknown) => void;
7
7
  }
8
8
 
9
9
  export async function writeCloudSkillWithBundleFallback(
10
10
  skill: CloudSkill,
11
11
  dependencies: WriteCloudSkillDependencies,
12
- ): Promise<boolean> {
12
+ ): Promise<SkillWriteOutcome | boolean> {
13
13
  if (skill.skill_source_url) {
14
14
  try {
15
15
  const bundleBytes = await dependencies.downloadSkillBundle(skill.skill_source_url);
16
16
  const wroteBundleToDisk = await dependencies.writeCloudSkillToDisk(skill, bundleBytes);
17
17
  if (wroteBundleToDisk) {
18
- return true;
18
+ return wroteBundleToDisk;
19
19
  }
20
20
 
21
21
  dependencies.onWarning?.(
@@ -0,0 +1,109 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { isValidProfileName, loadConfig } from './profiles.js';
8
+ import { withSkillSyncLock } from './sync-skills.js';
9
+
10
+ export const SKILL_SYNC_SERVICE_LABEL = 'ai.notis.skills-sync';
11
+ const bundlePath = fileURLToPath(new URL('../../dist/skill-sync-worker.mjs', import.meta.url));
12
+
13
+ function atomicWrite(target, value, mode = 0o600) {
14
+ mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
15
+ const temporary = `${target}.${process.pid}.tmp`;
16
+ writeFileSync(temporary, value, { mode });
17
+ renameSync(temporary, target);
18
+ }
19
+
20
+ function xml(value) {
21
+ return String(value).replaceAll('&', '&amp;').replaceAll('<', '&lt;')
22
+ .replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&apos;');
23
+ }
24
+
25
+ export async function maybeInstallSkillSyncService(runtime, {
26
+ config = loadConfig(), install = installSkillSyncService,
27
+ fetchSettings, refresh,
28
+ } = {}) {
29
+ if (process.platform !== 'darwin' || runtime.credentialKind !== 'oauth'
30
+ || config.current_profile !== runtime.profileName
31
+ || !['https://api.notis.ai', 'https://api-beta.notis.ai'].includes(runtime.apiBase)) return;
32
+ // Ordinary commands must not steal another explicitly bound account's job.
33
+ const plist = join(homedir(), 'Library', 'LaunchAgents', `${SKILL_SYNC_SERVICE_LABEL}.plist`);
34
+ if (existsSync(plist)) {
35
+ const saved = readFileSync(plist, 'utf8');
36
+ if (runtime.oauthUserId && [runtime.profileName, runtime.apiBase, runtime.oauthUserId]
37
+ .every(value => saved.includes(`<string>${xml(value)}</string>`))) return install(runtime);
38
+ return;
39
+ }
40
+ const refreshCredential = refresh || (await import('./oauth.js')).ensureFreshOAuthCredential;
41
+ await refreshCredential(runtime);
42
+ const getSettings = fetchSettings || (await import('../../dist/skill-sync/index.js')).fetchSyncSettings;
43
+ const settings = await getSettings(runtime.apiBase, runtime.jwt);
44
+ if (settings.sync_enabled && settings.user_id === runtime.oauthUserId) return install(runtime);
45
+ }
46
+
47
+ export async function installSkillSyncService(runtime, options = {}) {
48
+ if ((options.platform || process.platform) !== 'darwin') return { status: 'unsupported_platform' };
49
+ if (runtime.credentialKind !== 'oauth' || !runtime.oauthUserId
50
+ || !['https://api.notis.ai', 'https://api-beta.notis.ai'].includes(runtime.apiBase)) {
51
+ return { status: 'skipped_non_personal_profile' };
52
+ }
53
+ // Registration upgrades must not kill a worker holding the filesystem lock,
54
+ // nor race another CLI registering the same LaunchAgent.
55
+ return withSkillSyncLock(() => installSkillSyncServiceLocked(runtime, options),
56
+ options.home ? { home: options.home } : {});
57
+ }
58
+
59
+ async function installSkillSyncServiceLocked(runtime, {
60
+ home = homedir(), platform = process.platform, nodePath = process.execPath,
61
+ source = bundlePath, run = spawnSync, uid = process.getuid?.(),
62
+ } = {}) {
63
+ // A single explicitly selected personal account owns global agent folders.
64
+ // Development and hosted credentials must never enroll a machine-wide job.
65
+ if (platform !== 'darwin') return { status: 'unsupported_platform' };
66
+ if (runtime.credentialKind !== 'oauth' || runtime.envCredentialOverride
67
+ || !isValidProfileName(runtime.profileName)
68
+ || !['https://api.notis.ai', 'https://api-beta.notis.ai'].includes(runtime.apiBase)) {
69
+ return { status: 'skipped_non_personal_profile' };
70
+ }
71
+ const root = join(home, '.notis', 'skills', 'service');
72
+ const bundle = readFileSync(source);
73
+ const digest = createHash('sha256').update(bundle).digest('hex');
74
+ const installedBundle = join(root, 'runtime', digest, 'worker.mjs');
75
+ if (!existsSync(installedBundle)
76
+ || !readFileSync(installedBundle).equals(bundle)) atomicWrite(installedBundle, bundle, 0o500);
77
+ const args = [nodePath, installedBundle, runtime.profileName, runtime.apiBase, runtime.oauthUserId];
78
+ if (!runtime.oauthUserId) return { status: 'skipped_missing_account' };
79
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
80
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
81
+ <plist version="1.0"><dict>
82
+ <key>Label</key><string>${SKILL_SYNC_SERVICE_LABEL}</string>
83
+ <key>ProgramArguments</key><array>${args.map(value => `<string>${xml(value)}</string>`).join('')}</array>
84
+ <key>WorkingDirectory</key><string>${xml(root)}</string>
85
+ <key>StartInterval</key><integer>60</integer>
86
+ <key>ProcessType</key><string>Background</string>
87
+ </dict></plist>
88
+ `;
89
+ const plistPath = join(home, 'Library', 'LaunchAgents', `${SKILL_SYNC_SERVICE_LABEL}.plist`);
90
+ const domain = `gui/${uid}`;
91
+ const target = `${domain}/${SKILL_SYNC_SERVICE_LABEL}`;
92
+ const unchanged = existsSync(plistPath) && readFileSync(plistPath, 'utf8') === plist;
93
+ const loaded = run('/bin/launchctl', ['print', target], { encoding: 'utf8', timeout: 5000 });
94
+ if (unchanged && loaded.status === 0) return { status: 'installed', intervalSeconds: 60, profile: runtime.profileName };
95
+ if (loaded.status === 0) {
96
+ const stopped = run('/bin/launchctl', ['bootout', target], { encoding: 'utf8', timeout: 5000 });
97
+ if (stopped.status !== 0) throw new Error('Could not update the existing automatic skill sync job');
98
+ }
99
+ atomicWrite(plistPath, plist);
100
+ let started;
101
+ // launchd may finish tearing down an old registration after bootout returns.
102
+ for (let attempt = 0; attempt < 10; attempt++) {
103
+ started = run('/bin/launchctl', ['bootstrap', domain, plistPath], { encoding: 'utf8', timeout: 5000 });
104
+ if (started.status === 0) break;
105
+ await new Promise(resolve => setTimeout(resolve, 100));
106
+ }
107
+ if (started?.status !== 0) throw new Error('Could not register automatic skill sync with macOS');
108
+ return { status: 'installed', intervalSeconds: 60, profile: runtime.profileName };
109
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Commands that write inside a source folder first prove the folder is one.
3
+ *
4
+ * 2026-10-02: `notis spaces build .` run from the Notis repository root treated
5
+ * the repository as a Space source, refreshed "its" embedded SDK and deleted the
6
+ * canonical packages/sdk/src/presentation.tsx. A source folder is identified by
7
+ * a marker at its root, checked before any file is read for writing, staged,
8
+ * refreshed or deleted.
9
+ */
10
+ import { statSync } from 'node:fs';
11
+ import { join, resolve } from 'node:path';
12
+
13
+ import { usageError } from './errors.js';
14
+
15
+ // The workspace Space index (also a legacy App's config) or a pulled Space's lock.
16
+ export const SOURCE_ROOT_MARKERS = Object.freeze(['notis.config.ts', 'notis.config.js', 'notis.config.mjs', '.notis/space-lock.json']);
17
+
18
+ export function isSourceRoot(directory) {
19
+ return SOURCE_ROOT_MARKERS.some((name) => {
20
+ try { return statSync(join(directory, name)).isFile(); } catch { return false; }
21
+ });
22
+ }
23
+
24
+ /** Return the resolved folder, or refuse before anything inside it is touched. */
25
+ export function assertSourceRoot(directory) {
26
+ const root = resolve(directory);
27
+ if (!isSourceRoot(root)) {
28
+ throw usageError(`${root} is not a Space source: it has no notis.config.ts (or .js, .mjs) and no .notis/space-lock.json `
29
+ + 'at its root, so nothing was changed. Run the command in a Space source folder, or pass that folder.');
30
+ }
31
+ return root;
32
+ }
@@ -0,0 +1,107 @@
1
+ import { writeFileSync, mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { scaffoldProject, syncSpaceSdk } from './app-platform.js';
4
+ import { validateViewPath } from './space-view-manifest.js';
5
+ import { usageError } from './errors.js';
6
+
7
+ /** A local, editable collection layout. No remote database or Space mutation. */
8
+ export async function scaffoldSpaceCollection({ projectDir, name, databaseKey, path = 'records', titleProperty = 'title' }) {
9
+ if (typeof databaseKey !== 'string' || !/^[a-z][a-z0-9_-]{0,99}$/.test(databaseKey)) throw usageError('Pass the linked database alias as --database-key: start with a lowercase letter, then use lowercase letters, digits, underscores or hyphens (up to 100 characters).');
10
+ if (typeof name !== 'string' || !name.trim() || typeof titleProperty !== 'string' || !titleProperty.trim()) throw usageError('Choose a Space name and title property.');
11
+ validateViewPath(path);
12
+ await scaffoldProject({ projectDir, appName: name });
13
+ // A Space installs only the SDK's types and build-time entries; the host provides the rest.
14
+ syncSpaceSdk(projectDir, { log: () => {} });
15
+ mkdirSync(join(projectDir, 'spaces', 'collection'), { recursive: true });
16
+ writeFileSync(join(projectDir, 'notis.config.ts'), `import { defineSpaces } from '@notis/sdk/config';
17
+ export default defineSpaces({ spaces: { collection: { definition: 'spaces/collection/space.config.ts' } } });\n`);
18
+ const definition = { specVersion: 2, name, description: `Browse and edit ${name} records.`, path, entry: './view.tsx', layout: '../../app/layout.tsx',
19
+ readableContext: `Browse ${name} records. The item parameter opens one record with its properties, collaborative body and sharing controls.`,
20
+ verificationFixtures: './verification.json',
21
+ resources: { [databaseKey]: { kind: 'database', key: databaseKey } },
22
+ collection: { database: databaseKey, titleProperty },
23
+ params: { item: { type: 'record', database: databaseKey, main: true, description: 'The record opened beside the list.' } },
24
+ shows: { items: { database: databaseKey, where: {}, open: 'item' } },
25
+ memory: { markdown: true, attachments: true, screenshot: false, snapshots: [] },
26
+ // Each action is the linked database's own query, insert or trash: Notis turns it into the binding operation
27
+ // that useNativeMutation calls, so the view never names an account-wide database.
28
+ actions: { read: rowAction('LOCAL_NOTIS_DATABASE_QUERY', databaseKey), create: rowAction('LOCAL_NOTIS_DATABASE_INSERT', databaseKey),
29
+ delete: rowAction('LOCAL_NOTIS_DATABASE_DELETE', databaseKey) },
30
+ };
31
+ writeFileSync(join(projectDir, 'spaces/collection/space.config.ts'), `import { defineSpace } from '@notis/sdk/config';\nexport default defineSpace(${JSON.stringify(definition, null, 2)});\n`);
32
+ writeFileSync(join(projectDir, 'spaces/collection/view.tsx'), collectionView(databaseKey));
33
+ writeFileSync(join(projectDir, 'spaces/collection/verification.json'), JSON.stringify({
34
+ actions: {}, shown: { items: [{ params: {}, result: { rows: [], has_more: false, schema_revision: 1 } }] },
35
+ }, null, 2) + '\n');
36
+ return { projectDir, key: 'collection', databaseKey };
37
+ }
38
+
39
+ /** One declared action over the linked database: the tool gets the binding and the view's request, nothing else. */
40
+ function rowAction(tool, databaseKey) {
41
+ return { tool, inputs: { type: 'object', additionalProperties: false, required: ['request'], properties: { request: { type: 'object' } } },
42
+ arguments: { database_id: { $asset: databaseKey }, request: { $input: 'request' } } };
43
+ }
44
+
45
+ /**
46
+ * The scaffolded view: one ListReader over the linked records, with the host top bar search, an All pill, a "+" that
47
+ * creates and opens a record, and the record's document page in the reader. The title lives in the host top bar.
48
+ */
49
+ function collectionView(databaseKey) {
50
+ return `'use client';
51
+ import { useRef } from 'react';
52
+ import { DocumentPage, useNativeMutation, useShown, useViewParams } from '@notis/sdk';
53
+ import { EmptyState, ListReader, Notice, Row, type ListReaderApi } from '@notis/sdk/ui';
54
+ import definition from './space.config';
55
+
56
+ type Item = { record_key: string; revision: number; title: string | null; last_edited_time: string | null };
57
+
58
+ /** The view's words. Spaces are English only; the toolkit's own words follow the Notis language. */
59
+ const t = { list: 'Records', search: 'Search records…', all: 'All', create: 'New record', untitled: 'Untitled',
60
+ empty: 'No records yet.', remove: 'Delete', failed: 'Could not create the record. Try again.' };
61
+
62
+ export default function CollectionView() {
63
+ const { setParams } = useViewParams<typeof definition>();
64
+ const { data, loading, error, refetch } = useShown<Item>('items', { allPages: true });
65
+ const insert = useNativeMutation(${JSON.stringify(databaseKey)}, 'insert');
66
+ const trash = useNativeMutation(${JSON.stringify(databaseKey)}, 'delete');
67
+ const list = useRef<ListReaderApi>(null);
68
+ const rows = data?.rows ?? [];
69
+ const write = () => ({ schemaRevision: data?.schema_revision ?? 0, requestId: crypto.randomUUID() });
70
+ const title = (record: Item) => record.title || t.untitled;
71
+
72
+ async function create() {
73
+ try {
74
+ const receipt = await insert.mutate({}, write());
75
+ await refetch();
76
+ await setParams({ item: receipt.record_key });
77
+ } catch {
78
+ list.current?.notify(t.failed);
79
+ }
80
+ }
81
+
82
+ async function remove(records: Item[]) {
83
+ for (const record of records) {
84
+ await trash.mutate({ record_key: record.record_key, expected_revision: record.revision }, write());
85
+ }
86
+ await refetch();
87
+ }
88
+
89
+ // The host top bar shows the Space and view name; the open record joins it as the last crumb.
90
+ return <ListReader apiRef={list} label={t.list} items={rows} getId={record => record.record_key} openParam="item"
91
+ loading={loading}
92
+ search={{ placeholder: t.search, match: (record, query) => title(record).toLocaleLowerCase().includes(query.toLocaleLowerCase()) }}
93
+ // One real view so far; add a pill per saved filter once the database has one.
94
+ tabs={{ role: 'filter', items: [{ id: 'all', label: t.all, count: rows.length }], value: 'all', onChange: () => undefined }}
95
+ create={{ label: t.create, onCreate: () => void create() }}
96
+ actions={[{ id: 'delete', run: remove }]}
97
+ empty={error ? <Notice tone="danger" onRetry={() => void refetch()}>{error.message}</Notice> : <EmptyState title={t.empty} />}
98
+ row={record => <Row title={title(record)} time={record.last_edited_time ?? undefined} />}
99
+ readerLabel={title}
100
+ crumbLabel={title}
101
+ readerActions={(_record, ctx) => [{ id: 'delete', label: t.remove, icon: 'trash', run: () => void ctx.run('delete') }]}
102
+ reader={record => <DocumentPage key={record.record_key} recordKey={record.record_key} onSaved={refetch} />} />;
103
+ }
104
+ `;
105
+ }
106
+
107
+ export const COLLECTION_VIEW = collectionView('items');