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