@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
@@ -1,3 +1,8 @@
1
+ // src/runtime/skill-sync/index.ts
2
+ import { createHash as createHash4 } from "node:crypto";
3
+ import { promises as fs3 } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+
1
6
  // src/runtime/skill-sync/types.ts
2
7
  var DEFAULT_AGENT_TARGETS = {
3
8
  notis: true,
@@ -14,8 +19,133 @@ function normalizeAgentTargets(targets) {
14
19
  };
15
20
  }
16
21
 
22
+ // src/runtime/skill-sync/native-identity.ts
23
+ import { createHash } from "node:crypto";
24
+ var NATIVE_IDENTITY_FILE = ".notis-native-skill.json";
25
+ var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
26
+ function checkedNativeReference(value, cloudId) {
27
+ const reference = value;
28
+ if (!reference || typeof reference !== "object" || Array.isArray(reference) || Object.keys(reference).sort().join(",") !== "access_revision,kind,skill_id,version" || reference.kind !== "native_standalone_skill" || !UUID.test(reference.skill_id) || cloudId !== void 0 && reference.skill_id !== cloudId || !Number.isSafeInteger(reference.access_revision) || reference.access_revision < 0 || !reference.version || Object.keys(reference.version).sort().join(",") !== "digest,revision" || !Number.isSafeInteger(reference.version.revision) || reference.version.revision < 1 || !/^[a-f0-9]{64}$/.test(reference.version.digest)) {
29
+ throw new Error("Native Skill identity is unavailable. Refresh before syncing.");
30
+ }
31
+ return structuredClone(reference);
32
+ }
33
+ function nativeDirectory(reference) {
34
+ return "native-" + checkedNativeReference(reference).skill_id;
35
+ }
36
+ function sameNativeReference(left, right) {
37
+ return Boolean(left && right && left.skill_id === right.skill_id && left.access_revision === right.access_revision && left.version.revision === right.version.revision && left.version.digest === right.version.digest);
38
+ }
39
+ function isNativeSkill(skill) {
40
+ return skill.source === "native" || skill.native_reference !== void 0;
41
+ }
42
+ function assertNativePull(response) {
43
+ const ids = /* @__PURE__ */ new Set();
44
+ for (const skill of response.skills) {
45
+ if (!isNativeSkill(skill)) continue;
46
+ const reference = checkedNativeReference(skill.native_reference, skill.id);
47
+ if (response.skill_sync_protocol !== 2 || skill.name !== nativeDirectory(reference) || ids.has(skill.id) || !/^[a-f0-9]{64}$/.test(response.sync_scope || "") || skill.native_scope !== response.sync_scope || !Array.isArray(skill.bundle_files) || skill.bundle_hydration_failed || typeof skill.skill_md !== "string") {
48
+ throw new Error("Native Skill inventory is incomplete. Existing local files were retained.");
49
+ }
50
+ const paths = /* @__PURE__ */ new Set();
51
+ let entrypoint = false;
52
+ for (const file of skill.bundle_files) {
53
+ if (typeof file.path !== "string" || typeof file.content_b64 !== "string" || file.path.startsWith("/") || file.path.includes("\\") || file.path.includes("\0") || file.path.split("/").some((part) => !part || part === "." || part === "..") || paths.has(file.path.toLowerCase()) || file.path.toLowerCase() === NATIVE_IDENTITY_FILE) {
54
+ throw new Error("Native Skill bundle has a conflicting managed path.");
55
+ }
56
+ paths.add(file.path.toLowerCase());
57
+ if (file.path === "SKILL.md") {
58
+ entrypoint = Buffer.from(file.content_b64, "base64").equals(Buffer.from(skill.skill_md, "utf8"));
59
+ }
60
+ }
61
+ if (!entrypoint) throw new Error("Native Skill instructions and files do not match.");
62
+ ids.add(skill.id);
63
+ }
64
+ }
65
+ function nativeFileDigest(bytes) {
66
+ return createHash("sha256").update(bytes).digest("hex");
67
+ }
68
+ function nativeFrontmatterName(markdown) {
69
+ const header = markdown.match(/^---\s*\n([\s\S]*?)\n---/);
70
+ return header?.[1].match(/^name\s*:\s*(.+)$/m)?.[1].trim().replace(/^['"]|['"]$/g, "") || "";
71
+ }
72
+ function checkedNativeIdentity(value) {
73
+ const identity = value;
74
+ if (!identity || identity.schema !== 1 || typeof identity.cloudId !== "string" || typeof identity.displayName !== "string" || typeof identity.frontmatterName !== "string" || !/^[a-f0-9]{64}$/.test(identity.scope) || !/^[a-f0-9]{64}$/.test(identity.folderHash) || !identity.files || !Object.keys(identity.files).includes("SKILL.md") || Object.entries(identity.files).some(([name, hash]) => name.startsWith("/") || name.includes("\\") || name.split("/").some((part) => !part || part === "." || part === "..") || !/^[a-f0-9]{64}$/.test(hash))) {
75
+ throw new Error("Native Skill provenance is incomplete. Existing files were retained.");
76
+ }
77
+ return { ...identity, reference: checkedNativeReference(identity.reference, identity.cloudId) };
78
+ }
79
+ function nativeState(skill) {
80
+ if (!isNativeSkill(skill)) return {};
81
+ return {
82
+ nativeReference: checkedNativeReference(skill.native_reference, skill.id),
83
+ ...skill.native_settings ? { nativeSettingsRevision: skill.native_settings.revision } : {},
84
+ nativeDisplayName: skill.display_name || skill.name,
85
+ nativeScope: skill.native_scope,
86
+ nativeUnavailable: false
87
+ };
88
+ }
89
+
90
+ // src/runtime/skill-sync/native-plan.ts
91
+ function bindNativeFolders(response, locals, state) {
92
+ const nativeIds = new Set(response.skills.filter(isNativeSkill).map((skill) => skill.id));
93
+ for (const entry of Object.values(state.skills)) if (entry.nativeReference || entry.nativeUnavailable) nativeIds.add(entry.cloudId);
94
+ const byId = /* @__PURE__ */ new Map();
95
+ for (const local of locals) {
96
+ const id = local.cloudId || state.skills[local.name]?.cloudId;
97
+ if (!id || !nativeIds.has(id) && !local.nativeReference) continue;
98
+ if (local.nativeReference) checkedNativeReference(local.nativeReference, id);
99
+ if (byId.has(id)) throw new Error("Two local folders claim the same native Skill. Keep both copies and resolve the conflict.");
100
+ byId.set(id, local);
101
+ }
102
+ const skills = response.skills.map((skill) => isNativeSkill(skill) && byId.has(skill.id) ? { ...skill, name: byId.get(skill.id).name } : skill);
103
+ for (const skill of skills.filter(isNativeSkill)) {
104
+ if (skills.some((other) => other.id !== skill.id && other.name === skill.name)) {
105
+ throw new Error("A native Skill and another Skill share a local folder. Existing files were retained.");
106
+ }
107
+ }
108
+ return { ...response, skills };
109
+ }
110
+ function previousNativeState(state, skill) {
111
+ const named = state.skills[skill.name];
112
+ return named?.cloudId === skill.id ? named : Object.values(state.skills).find((entry) => entry.cloudId === skill.id);
113
+ }
114
+ function nativeLocalReady(skill, local) {
115
+ return Boolean(local?.nativeFilesCurrent && local.folderHash === local.nativeBaseFolderHash && local.cloudId === skill.id && local.nativeScope === skill.native_scope && sameNativeReference(local.nativeReference, skill.native_reference));
116
+ }
117
+ function pendingLegacyUpgrade(skill, local, state) {
118
+ if (!isNativeSkill(skill) || !local || local.nativeReference || local.cloudId) return false;
119
+ const previous = previousNativeState(state, skill);
120
+ return Boolean(previous && !previous.nativeReference);
121
+ }
122
+ function nativeLinkRows(response, locals, state) {
123
+ return response.skills.map((skill) => {
124
+ const local = locals.find((row) => row.name === skill.name);
125
+ return isNativeSkill(skill) && !nativeLocalReady(skill, local) && !(state && pendingLegacyUpgrade(skill, local, state)) ? { ...skill, status: "disabled" } : skill;
126
+ });
127
+ }
128
+ function retainMissingNativeState(previous, next) {
129
+ const present = new Set(Object.values(next.skills).map((row) => row.cloudId));
130
+ const retained = Object.fromEntries(Object.entries(previous.skills).filter(([, row]) => (row.nativeReference || row.nativeUnavailable) && !present.has(row.cloudId)).map(([name, row]) => [name, { ...row, nativeUnavailable: true, verifiedAgentLinks: {} }]));
131
+ const history = structuredClone(previous.nativeHistory || {});
132
+ for (const [name, row] of [...Object.entries(previous.skills), ...Object.entries(next.skills)]) {
133
+ if (!row.nativeReference && !row.nativeUnavailable) continue;
134
+ const names = history[row.cloudId]?.names || [];
135
+ history[row.cloudId] = { names: [.../* @__PURE__ */ new Set([...names, name])] };
136
+ }
137
+ return {
138
+ ...next,
139
+ skills: { ...retained, ...next.skills },
140
+ ...Object.keys(history).length ? { nativeHistory: history } : {}
141
+ };
142
+ }
143
+ function hasNativeFolderHistory(state, name) {
144
+ return Object.values(state.nativeHistory || {}).some((row) => row.names.includes(name));
145
+ }
146
+
17
147
  // src/runtime/skill-sync/local-scanner.ts
18
- import { createHash } from "crypto";
148
+ import { createHash as createHash2 } from "crypto";
19
149
  import { execFile } from "child_process";
20
150
  import { promises as fs } from "fs";
21
151
  import os from "os";
@@ -23,6 +153,8 @@ import path from "path";
23
153
  import { promisify } from "util";
24
154
  var HOME_DIR = os.homedir();
25
155
  var execFileAsync = promisify(execFile);
156
+ var NativeSkillIdentityError = class extends Error {
157
+ };
26
158
  var AGENTS_DIR = path.join(HOME_DIR, ".agents");
27
159
  var LEGACY_AGENTS_SKILLS_DIR = path.join(AGENTS_DIR, "skills");
28
160
  var NOTIS_SKILL_SYNC_ROOT = path.join(HOME_DIR, ".notis", "skills");
@@ -82,15 +214,18 @@ function sanitizePathSegment(value) {
82
214
  if (sanitized === raw) {
83
215
  return sanitized;
84
216
  }
85
- return `${sanitized}-${createHash("sha256").update(raw).digest("hex").slice(0, 12)}`;
217
+ return `${sanitized}-${createHash2("sha256").update(raw).digest("hex").slice(0, 12)}`;
86
218
  }
87
- function getSkillSyncPathsForUser(authUserId, agentsDir = AGENTS_DIR) {
219
+ function getSkillSyncPathsForUser(authUserId, agentsDir = AGENTS_DIR, namespace, syncHome) {
88
220
  const safeUserId = sanitizePathSegment(authUserId);
89
- const resolvedAgentsDir = path.resolve(agentsDir);
90
- const syncRoot = getDefaultSyncRootForAgentsDir(resolvedAgentsDir);
91
- const userRoot = path.join(syncRoot, "users", safeUserId);
221
+ const homeDir = syncHome ? path.resolve(syncHome) : void 0;
222
+ const resolvedAgentsDir = homeDir ? path.join(homeDir, ".agents") : path.resolve(agentsDir);
223
+ const syncRoot = homeDir ? path.join(homeDir, ".notis", "skills") : getDefaultSyncRootForAgentsDir(resolvedAgentsDir);
224
+ const userRoot = namespace ? path.join(syncRoot, "environments", sanitizePathSegment(namespace), "users", safeUserId) : path.join(syncRoot, "users", safeUserId);
92
225
  const legacyUserRoot = path.join(resolvedAgentsDir, "notis", "users", safeUserId);
93
226
  return {
227
+ ...homeDir ? { homeDir } : {},
228
+ ...namespace ? { isolatedNamespace: sanitizePathSegment(namespace) } : {},
94
229
  agentsDir: resolvedAgentsDir,
95
230
  syncRoot,
96
231
  legacySkillsDir: path.join(resolvedAgentsDir, "skills"),
@@ -139,16 +274,23 @@ async function readJsonFile(filePath) {
139
274
  return null;
140
275
  }
141
276
  }
142
- async function listFilesRecursive(dirPath) {
277
+ var IGNORED_SKILL_DIRECTORIES = /* @__PURE__ */ new Set(["__pycache__", ".pytest_cache"]);
278
+ function isIgnoredSkillEntry(entry) {
279
+ if (entry.name === ".DS_Store") return true;
280
+ if (entry.isDirectory()) return IGNORED_SKILL_DIRECTORIES.has(entry.name);
281
+ return /\.py[co]$/i.test(entry.name);
282
+ }
283
+ async function listFilesRecursive(dirPath, rootPath = dirPath, strictLinks = false) {
143
284
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
144
285
  const nested = await Promise.all(
145
286
  entries.map(async (entry) => {
146
- if (entry.name === ".DS_Store") {
287
+ if (isIgnoredSkillEntry(entry) || dirPath === rootPath && entry.name === NATIVE_IDENTITY_FILE) {
147
288
  return [];
148
289
  }
149
290
  const fullPath = path.join(dirPath, entry.name);
291
+ if (strictLinks && entry.isSymbolicLink()) throw new Error("Native Skill bundles cannot contain symbolic links.");
150
292
  if (entry.isDirectory()) {
151
- return listFilesRecursive(fullPath);
293
+ return listFilesRecursive(fullPath, rootPath, strictLinks);
152
294
  }
153
295
  if (entry.isFile()) {
154
296
  return [fullPath];
@@ -159,7 +301,7 @@ async function listFilesRecursive(dirPath) {
159
301
  return nested.flat().sort();
160
302
  }
161
303
  async function computeFolderHash(dirPath) {
162
- const hash = createHash("sha256");
304
+ const hash = createHash2("sha256");
163
305
  const filePaths = await listFilesRecursive(dirPath);
164
306
  for (const filePath of filePaths) {
165
307
  const relativePath = path.relative(dirPath, filePath);
@@ -237,17 +379,17 @@ function defaultTopLevelSkillSources(paths) {
237
379
  { label: "agents", root: paths.legacySkillsDir, priority: 2 },
238
380
  {
239
381
  label: "codex",
240
- root: path.join(HOME_DIR, ".codex", "skills"),
382
+ root: path.join(paths.homeDir || HOME_DIR, ".codex", "skills"),
241
383
  priority: 3
242
384
  },
243
385
  {
244
386
  label: "cursor",
245
- root: path.join(HOME_DIR, ".cursor", "skills"),
387
+ root: path.join(paths.homeDir || HOME_DIR, ".cursor", "skills"),
246
388
  priority: 4
247
389
  },
248
390
  {
249
391
  label: "claude",
250
- root: path.join(HOME_DIR, ".claude", "skills"),
392
+ root: path.join(paths.homeDir || HOME_DIR, ".claude", "skills"),
251
393
  priority: 5
252
394
  }
253
395
  );
@@ -467,6 +609,7 @@ async function ensureCanonicalSkillsDir(paths = DEFAULT_SYNC_PATHS) {
467
609
  async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
468
610
  await ensureCanonicalSkillsDir(paths);
469
611
  const sourceUrls = await readSkillLock(paths);
612
+ const previousState = await readSyncState(paths);
470
613
  const entries = await fs.readdir(paths.skillsDir, { withFileTypes: true });
471
614
  const skills = await Promise.all(
472
615
  entries.map(async (entry) => {
@@ -475,6 +618,15 @@ async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
475
618
  }
476
619
  const skillDir = path.join(paths.skillsDir, entry.name);
477
620
  const skillMdPath = path.join(skillDir, "SKILL.md");
621
+ let marker = null;
622
+ try {
623
+ marker = await fs.readFile(path.join(skillDir, NATIVE_IDENTITY_FILE), "utf8");
624
+ } catch (error) {
625
+ if (error.code !== "ENOENT") {
626
+ throw new NativeSkillIdentityError("Native Skill provenance could not be read; existing files were retained.");
627
+ }
628
+ }
629
+ const knownNative = marker !== null || Boolean(previousState.skills[entry.name]?.nativeReference) || Object.values(previousState.nativeHistory || {}).some((row) => row.names.includes(entry.name));
478
630
  try {
479
631
  const skillMd = await fs.readFile(skillMdPath, "utf8");
480
632
  const { description } = parseFrontMatter(skillMd);
@@ -486,11 +638,38 @@ async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
486
638
  folderHash,
487
639
  directoryPath: skillDir
488
640
  };
641
+ if (marker !== null) {
642
+ let identity;
643
+ try {
644
+ identity = checkedNativeIdentity(JSON.parse(marker));
645
+ } catch {
646
+ throw new NativeSkillIdentityError("Native Skill provenance is unreadable; do not upload it as a new Skill.");
647
+ }
648
+ let current = true;
649
+ for (const [name, expected] of Object.entries(identity.files)) {
650
+ try {
651
+ if (nativeFileDigest(await fs.readFile(resolveSkillBundleFilePath(skillDir, name))) !== expected) current = false;
652
+ } catch {
653
+ current = false;
654
+ }
655
+ }
656
+ const editedName = nativeFrontmatterName(skillMd);
657
+ Object.assign(skill, {
658
+ cloudId: identity.cloudId,
659
+ nativeReference: identity.reference,
660
+ nativeScope: identity.scope,
661
+ nativeBaseFolderHash: identity.folderHash,
662
+ nativeFilesCurrent: current,
663
+ displayName: editedName && editedName !== identity.frontmatterName ? editedName : identity.displayName
664
+ });
665
+ }
489
666
  if (sourceUrls[entry.name]) {
490
667
  skill.sourceUrl = sourceUrls[entry.name];
491
668
  }
492
669
  return skill;
493
- } catch {
670
+ } catch (error) {
671
+ if (error instanceof NativeSkillIdentityError) throw error;
672
+ if (knownNative) throw new NativeSkillIdentityError("Native Skill files could not be read; existing files were retained.");
494
673
  return null;
495
674
  }
496
675
  })
@@ -501,6 +680,9 @@ function normalizeSyncState(state) {
501
680
  if (!state || state.version !== 1 || typeof state.skills !== "object") {
502
681
  return null;
503
682
  }
683
+ if (state.nativeHistory && (typeof state.nativeHistory !== "object" || Array.isArray(state.nativeHistory) || Object.values(state.nativeHistory).some((row) => !row || !Array.isArray(row.names) || row.names.some((name) => typeof name !== "string")))) {
684
+ throw new Error("Native Skill sync provenance is unreadable; existing files were retained.");
685
+ }
504
686
  const normalizeStoredAgentTargets = (targets) => ({
505
687
  notis: Boolean(targets?.notis ?? true),
506
688
  claude_code: Boolean(targets?.claude_code ?? true),
@@ -509,6 +691,7 @@ function normalizeSyncState(state) {
509
691
  });
510
692
  return {
511
693
  version: 1,
694
+ ...state.nativeHistory ? { nativeHistory: state.nativeHistory } : {},
512
695
  lastSyncedAt: typeof state.lastSyncedAt === "string" ? state.lastSyncedAt : null,
513
696
  skills: Object.fromEntries(
514
697
  Object.entries(state.skills || {}).map(([skillName, skillState]) => [
@@ -583,20 +766,49 @@ async function createZipFromDirectory(directoryPath) {
583
766
  os.tmpdir(),
584
767
  `notis-skill-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`
585
768
  );
586
- const parentDir = path.dirname(directoryPath);
587
769
  const directoryName = path.basename(directoryPath);
588
- if (process.platform === "win32") {
589
- await execFileAsync("powershell.exe", [
590
- "-NoProfile",
591
- "-Command",
592
- `Compress-Archive -Path '${directoryPath.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`
593
- ]);
770
+ const stagingRoot = await fs.mkdtemp(path.join(os.tmpdir(), "notis-skill-bundle-"));
771
+ try {
772
+ const stagedDir = path.join(stagingRoot, directoryName);
773
+ for (const filePath of await listFilesRecursive(directoryPath)) {
774
+ const target = path.join(stagedDir, path.relative(directoryPath, filePath));
775
+ await fs.mkdir(path.dirname(target), { recursive: true });
776
+ await fs.copyFile(filePath, target);
777
+ }
778
+ await fs.mkdir(stagedDir, { recursive: true });
779
+ if (process.platform === "win32") {
780
+ const systemTar = path.join(
781
+ process.env.SystemRoot || "C:\\Windows",
782
+ "System32",
783
+ "tar.exe"
784
+ );
785
+ if (await pathExists(systemTar)) {
786
+ await execFileAsync(systemTar, [
787
+ "-c",
788
+ "--format",
789
+ "zip",
790
+ "-f",
791
+ zipPath,
792
+ "-C",
793
+ stagingRoot,
794
+ directoryName
795
+ ]);
796
+ return zipPath;
797
+ }
798
+ await execFileAsync("powershell.exe", [
799
+ "-NoProfile",
800
+ "-Command",
801
+ `Compress-Archive -Path '${stagedDir.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`
802
+ ]);
803
+ return zipPath;
804
+ }
805
+ await execFileAsync("zip", ["-qry", zipPath, directoryName], {
806
+ cwd: stagingRoot
807
+ });
594
808
  return zipPath;
809
+ } finally {
810
+ await fs.rm(stagingRoot, { recursive: true, force: true });
595
811
  }
596
- await execFileAsync("zip", ["-qry", zipPath, directoryName], {
597
- cwd: parentDir
598
- });
599
- return zipPath;
600
812
  }
601
813
  async function extractZipToDirectory(zipPath, destinationDir) {
602
814
  const extractRoot = await fs.mkdtemp(
@@ -622,10 +834,39 @@ async function extractZipToDirectory(zipPath, destinationDir) {
622
834
  await fs.rm(destinationDir, { recursive: true, force: true });
623
835
  await fs.mkdir(path.dirname(destinationDir), { recursive: true });
624
836
  await fs.cp(sourceDir, destinationDir, { recursive: true });
837
+ return await listRelativeFiles(destinationDir);
625
838
  } finally {
626
839
  await fs.rm(extractRoot, { recursive: true, force: true });
627
840
  }
628
841
  }
842
+ async function listRelativeFiles(dirPath) {
843
+ try {
844
+ const files = await listFilesRecursive(dirPath);
845
+ return files.map((filePath) => toPosixRelativePath(dirPath, filePath)).sort();
846
+ } catch (error) {
847
+ if (error?.code === "ENOENT") return [];
848
+ throw error;
849
+ }
850
+ }
851
+ function toPosixRelativePath(rootDir, filePath) {
852
+ return path.relative(rootDir, filePath).split(path.sep).join("/");
853
+ }
854
+ async function preserveUnmanagedFiles(existingDir, stagingDir, previouslyApplied, strictConflicts = false) {
855
+ const managed = previouslyApplied ? new Set(previouslyApplied) : null;
856
+ for (const existingFile of await listFilesRecursive(existingDir)) {
857
+ const relativePath = toPosixRelativePath(existingDir, existingFile);
858
+ if (managed?.has(relativePath)) continue;
859
+ const stagedPath = path.join(stagingDir, ...relativePath.split("/"));
860
+ if (await pathExists(stagedPath)) {
861
+ if (strictConflicts && !(await fs.readFile(existingFile)).equals(await fs.readFile(stagedPath))) {
862
+ throw new Error("An incoming native Skill file conflicts with a local file. Local files were retained.");
863
+ }
864
+ continue;
865
+ }
866
+ await fs.mkdir(path.dirname(stagedPath), { recursive: true });
867
+ await fs.copyFile(existingFile, stagedPath);
868
+ }
869
+ }
629
870
  async function pathExists(targetPath) {
630
871
  try {
631
872
  await fs.access(targetPath);
@@ -634,7 +875,7 @@ async function pathExists(targetPath) {
634
875
  return false;
635
876
  }
636
877
  }
637
- async function replaceSkillDirectoryAtomically(skillDir, populateDir) {
878
+ async function replaceSkillDirectoryAtomically(skillDir, populateDir, preserve) {
638
879
  const parentDir = path.dirname(skillDir);
639
880
  const skillName = path.basename(skillDir);
640
881
  await fs.mkdir(parentDir, { recursive: true });
@@ -649,10 +890,46 @@ async function replaceSkillDirectoryAtomically(skillDir, populateDir) {
649
890
  let promotedStaging = false;
650
891
  let cleanupError = null;
651
892
  try {
893
+ if (preserve?.native && preserve.expectedFolderHash === void 0) {
894
+ let existing = false;
895
+ try {
896
+ await fs.lstat(skillDir);
897
+ existing = true;
898
+ } catch (error) {
899
+ if (error.code !== "ENOENT") throw error;
900
+ }
901
+ if (existing) throw new Error("Existing native Skill files need a readable baseline before replacement.");
902
+ }
903
+ if (preserve?.expectedFolderHash !== void 0 && (!await pathExists(skillDir) || await computeFolderHash(skillDir) !== preserve.expectedFolderHash)) {
904
+ throw new Error("Local Skill files changed before sync; the newer local edits were retained.");
905
+ }
652
906
  await populateDir(stagingDir);
907
+ if (preserve && await pathExists(skillDir)) {
908
+ await preserveUnmanagedFiles(
909
+ skillDir,
910
+ stagingDir,
911
+ preserve.previouslyApplied,
912
+ preserve.native && !preserve.legacyBaseline
913
+ );
914
+ }
915
+ if (preserve?.native) {
916
+ const markerPath = path.join(stagingDir, NATIVE_IDENTITY_FILE);
917
+ const marker = checkedNativeIdentity(JSON.parse(await fs.readFile(markerPath, "utf8")));
918
+ marker.folderHash = await computeFolderHash(stagingDir);
919
+ await fs.writeFile(markerPath, JSON.stringify(marker), { mode: 384 });
920
+ }
921
+ if (preserve?.expectedFolderHash !== void 0 && (!await pathExists(skillDir) || await computeFolderHash(skillDir) !== preserve.expectedFolderHash)) {
922
+ throw new Error("Local Skill files changed during sync; the newer local edits were retained.");
923
+ }
653
924
  if (await pathExists(skillDir)) {
654
925
  await fs.rename(skillDir, backupDir);
655
926
  movedExisting = true;
927
+ if (preserve?.native && preserve.expectedFolderHash === void 0) {
928
+ throw new Error("Native Skill files appeared during sync; existing files were retained.");
929
+ }
930
+ if (preserve?.expectedFolderHash !== void 0 && await computeFolderHash(backupDir) !== preserve.expectedFolderHash) {
931
+ throw new Error("Local Skill files changed before publication; the newer local edits were retained.");
932
+ }
656
933
  }
657
934
  await fs.rename(stagingDir, skillDir);
658
935
  promotedStaging = true;
@@ -697,11 +974,43 @@ async function createSkillBundleBase64(skill) {
697
974
  await fs.rm(zipPath, { force: true });
698
975
  }
699
976
  }
700
- async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PATHS) {
977
+ async function captureNativeSkillFiles(skill) {
978
+ const hash = createHash2("sha256");
979
+ const files = [];
980
+ for (const filePath of await listFilesRecursive(skill.directoryPath, skill.directoryPath, true)) {
981
+ const relative = path.relative(skill.directoryPath, filePath);
982
+ const bytes = await fs.readFile(filePath);
983
+ hash.update(relative);
984
+ hash.update("\0");
985
+ hash.update(bytes);
986
+ hash.update("\0");
987
+ files.push({ path: relative.split(path.sep).join("/"), content_b64: bytes.toString("base64") });
988
+ }
989
+ if (hash.digest("hex") !== skill.folderHash) throw new Error("Local Skill files changed before upload. Sync again with the current files.");
990
+ return files;
991
+ }
992
+ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PATHS, options = {}) {
993
+ if (isNativeSkill(skill)) {
994
+ assertNativePull({
995
+ skills: [{ ...skill, name: "native-" + skill.id }],
996
+ skill_sync_protocol: 2,
997
+ sync_scope: skill.native_scope,
998
+ sync_enabled: true,
999
+ last_synced_at: null
1000
+ });
1001
+ if (bundleBytes?.length) throw new Error("Native Skills require their verified complete file response.");
1002
+ }
701
1003
  const skillDir = path.join(
702
1004
  paths.skillsDir,
703
1005
  safeName(skill.name, paths.skillsDir)
704
1006
  );
1007
+ const preserve = {
1008
+ previouslyApplied: options.previouslyApplied,
1009
+ expectedFolderHash: options.expectedFolderHash,
1010
+ native: isNativeSkill(skill),
1011
+ // An unchanged legacy folder upgrading in place: shared files take the cloud version.
1012
+ legacyBaseline: isNativeSkill(skill) && options.legacyBaseline === true && options.expectedFolderHash !== void 0
1013
+ };
705
1014
  if (bundleBytes?.length) {
706
1015
  const bundlePath = path.join(
707
1016
  os.tmpdir(),
@@ -709,8 +1018,11 @@ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PA
709
1018
  );
710
1019
  try {
711
1020
  await fs.writeFile(bundlePath, bundleBytes);
712
- await extractZipToDirectory(bundlePath, skillDir);
713
- return true;
1021
+ let appliedFiles = [];
1022
+ await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
1023
+ appliedFiles = await extractZipToDirectory(bundlePath, stagingDir);
1024
+ }, preserve);
1025
+ return { written: true, appliedFiles };
714
1026
  } finally {
715
1027
  await fs.rm(bundlePath, { force: true });
716
1028
  }
@@ -724,6 +1036,7 @@ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PA
724
1036
  `Synced bundle for "${skill.name}" is missing SKILL.md or SKILLS.md`
725
1037
  );
726
1038
  }
1039
+ const appliedFiles = [];
727
1040
  await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
728
1041
  for (const bundleFile of skill.bundle_files || []) {
729
1042
  const filePath = resolveSkillBundleFilePath(
@@ -735,9 +1048,24 @@ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PA
735
1048
  filePath,
736
1049
  Buffer.from(bundleFile.content_b64, "base64")
737
1050
  );
1051
+ appliedFiles.push(toPosixRelativePath(stagingDir, filePath));
738
1052
  }
739
- });
740
- return true;
1053
+ if (skill.native_reference) {
1054
+ const identity = {
1055
+ schema: 1,
1056
+ cloudId: skill.id,
1057
+ reference: skill.native_reference,
1058
+ displayName: skill.display_name || skill.name,
1059
+ frontmatterName: nativeFrontmatterName(skill.skill_md || ""),
1060
+ scope: skill.native_scope,
1061
+ folderHash: await computeFolderHash(stagingDir),
1062
+ files: Object.fromEntries((skill.bundle_files || []).map((file) => [file.path, nativeFileDigest(Buffer.from(file.content_b64, "base64"))]))
1063
+ };
1064
+ await fs.writeFile(path.join(stagingDir, NATIVE_IDENTITY_FILE), JSON.stringify(identity), { mode: 384 });
1065
+ appliedFiles.push(NATIVE_IDENTITY_FILE);
1066
+ }
1067
+ }, preserve);
1068
+ return { written: true, appliedFiles: appliedFiles.sort() };
741
1069
  }
742
1070
  await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
743
1071
  await fs.writeFile(
@@ -745,13 +1073,15 @@ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PA
745
1073
  skill.skill_md ?? "",
746
1074
  "utf8"
747
1075
  );
748
- });
749
- return true;
1076
+ }, preserve);
1077
+ return { written: true, appliedFiles: ["SKILL.md"] };
750
1078
  }
751
1079
 
752
1080
  // src/runtime/skill-sync/cloud-client.ts
1081
+ import { createHash as createHash3 } from "node:crypto";
753
1082
  async function requestJson(url, jwt, options = {}) {
754
1083
  const response = await fetch(url, {
1084
+ signal: AbortSignal.timeout(9e4),
755
1085
  method: options.method || "POST",
756
1086
  headers: {
757
1087
  "Content-Type": "application/json",
@@ -772,7 +1102,7 @@ async function fetchSyncSettings(serverUrl, jwt) {
772
1102
  }
773
1103
  async function pullSkills(serverUrl, jwt) {
774
1104
  return requestJson(`${serverUrl}/portal_skills/sync-pull`, jwt, {
775
- body: {}
1105
+ body: { skill_sync_protocol: 2 }
776
1106
  });
777
1107
  }
778
1108
  async function pushChangedSkills(serverUrl, jwt, changedSkills) {
@@ -782,28 +1112,55 @@ async function pushChangedSkills(serverUrl, jwt, changedSkills) {
782
1112
  skill_md: skill.skillMd,
783
1113
  source_url: skill.sourceUrl,
784
1114
  folder_hash: skill.folderHash,
785
- bundle_base64: await createSkillBundleBase64(skill)
1115
+ ...skill.nativeReference ? { bundle_files: await captureNativeSkillFiles(skill) } : { bundle_base64: await createSkillBundleBase64(skill) },
1116
+ ...skill.cloudId ? { cloud_id: skill.cloudId } : {},
1117
+ ...skill.nativeReference ? {
1118
+ native_reference: skill.nativeReference,
1119
+ display_name: skill.displayName,
1120
+ native_scope: skill.nativeScope
1121
+ } : {}
786
1122
  })));
787
1123
  return requestJson(`${serverUrl}/portal_skills/sync-push`, jwt, {
788
1124
  body: {
1125
+ skill_sync_protocol: 2,
789
1126
  skills: payloadSkills
790
1127
  }
791
1128
  });
792
1129
  }
793
1130
  async function downloadSkillBundle(bundleUrl) {
794
- const response = await fetch(bundleUrl);
1131
+ const response = await fetch(bundleUrl, { signal: AbortSignal.timeout(9e4) });
795
1132
  if (!response.ok) {
796
1133
  const text = await response.text();
797
1134
  throw new Error(`GET ${bundleUrl} \u2192 ${response.status}: ${text}`);
798
1135
  }
799
1136
  return Buffer.from(await response.arrayBuffer());
800
1137
  }
801
- async function updateAgentTargets(serverUrl, jwt, skillId, targets) {
1138
+ async function updateAgentTargets(serverUrl, jwt, skillId, targets, expectedUpdatedAt, native) {
1139
+ if (native) {
1140
+ checkedNativeReference(native.reference, skillId);
1141
+ if (!Number.isSafeInteger(native.settingsRevision) || native.settingsRevision < 0) throw new Error("Refresh native Skill settings before changing assignments.");
1142
+ const payload = {
1143
+ operation: "update",
1144
+ target: { skill_id: skillId, access_revision: native.reference.access_revision },
1145
+ revision: native.settingsRevision,
1146
+ patch: { agent_targets: Object.fromEntries(Object.entries(targets).sort(([a], [b]) => a.localeCompare(b))) }
1147
+ };
1148
+ const requestId = "sync-settings-" + createHash3("sha256").update(JSON.stringify(payload)).digest("hex");
1149
+ const saved = await requestJson(
1150
+ `${serverUrl}/portal_skills/native-settings`,
1151
+ jwt,
1152
+ { body: { ...payload, request_id: requestId } }
1153
+ );
1154
+ const current = saved.current || saved;
1155
+ if (saved.skill_id !== skillId || typeof saved.receipt_id !== "string" || !saved.receipt_id || current.target?.skill_id !== skillId || !current.settings) throw new Error("Native assignment update was not confirmed.");
1156
+ return { success: true, agent_targets: current.settings.agent_targets, updated_at: current.settings.updated_at };
1157
+ }
802
1158
  return requestJson(`${serverUrl}/portal_skills/agent-targets`, jwt, {
803
1159
  method: "PATCH",
804
1160
  body: {
805
1161
  skill_id: skillId,
806
- agent_targets: targets
1162
+ agent_targets: targets,
1163
+ ...expectedUpdatedAt ? { expected_updated_at: expectedUpdatedAt } : {}
807
1164
  }
808
1165
  });
809
1166
  }
@@ -819,6 +1176,18 @@ var EXTERNAL_AGENT_SKILL_DIRS = {
819
1176
  codex: path2.join(HOME_DIR2, ".codex", "skills")
820
1177
  };
821
1178
  var EXTERNAL_AGENTS = Object.keys(EXTERNAL_AGENT_SKILL_DIRS);
1179
+ var AGENT_FAILURE_LABELS = {
1180
+ claude_code: "Claude Code",
1181
+ cursor: "Cursor",
1182
+ codex: "Codex",
1183
+ legacy: "legacy ~/.agents/skills"
1184
+ };
1185
+ function agentFailureLabel(agent) {
1186
+ return AGENT_FAILURE_LABELS[agent] ?? agent;
1187
+ }
1188
+ function agentFolderFailureName(agent) {
1189
+ return `${agentFailureLabel(agent)} skills folder`;
1190
+ }
822
1191
  async function removeForeignAccountSymlinks(skillsDir, options = {}) {
823
1192
  const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
824
1193
  const currentRoot = path2.resolve(skillsDir);
@@ -879,8 +1248,9 @@ async function isManagedSymlink(linkPath, managedRoots) {
879
1248
  const target = await fs2.readlink(linkPath);
880
1249
  const resolvedTarget = path2.resolve(path2.dirname(linkPath), target);
881
1250
  return managedRoots.some((root) => resolvedTarget === root || resolvedTarget.startsWith(`${root}${path2.sep}`));
882
- } catch {
883
- return false;
1251
+ } catch (error) {
1252
+ if (error?.code === "ENOENT") return false;
1253
+ throw error;
884
1254
  }
885
1255
  }
886
1256
  async function ensureCorrectSymlink(linkPath, targetPath) {
@@ -896,7 +1266,8 @@ async function ensureCorrectSymlink(linkPath, targetPath) {
896
1266
  } else {
897
1267
  return "blocked";
898
1268
  }
899
- } catch {
1269
+ } catch (error) {
1270
+ if (error?.code !== "ENOENT") throw error;
900
1271
  }
901
1272
  const relativePath = path2.relative(path2.dirname(linkPath), targetPath);
902
1273
  await fs2.symlink(relativePath, linkPath);
@@ -908,19 +1279,26 @@ function defaultAgentSkillDirs(skillsDir) {
908
1279
  ...EXTERNAL_AGENT_SKILL_DIRS
909
1280
  };
910
1281
  }
911
- async function removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots) {
1282
+ async function removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots, failures, agent) {
912
1283
  let removed = 0;
913
1284
  try {
914
1285
  await fs2.mkdir(agentDir, { recursive: true });
915
1286
  const existingEntries = await fs2.readdir(agentDir, { withFileTypes: true });
916
1287
  for (const entry of existingEntries) {
917
1288
  const entryPath = path2.join(agentDir, entry.name);
918
- if (!desiredSkills.has(entry.name) && await isManagedSymlink(entryPath, managedRoots)) {
919
- await fs2.unlink(entryPath);
920
- removed += 1;
1289
+ try {
1290
+ if (!desiredSkills.has(entry.name) && await isManagedSymlink(entryPath, managedRoots)) {
1291
+ await fs2.unlink(entryPath);
1292
+ removed += 1;
1293
+ }
1294
+ } catch (error) {
1295
+ if (error?.code !== "ENOENT") {
1296
+ failures.push({ name: entry.name, error: `${agentFailureLabel(agent)}: could not remove skill link (${error.message})` });
1297
+ }
921
1298
  }
922
1299
  }
923
- } catch {
1300
+ } catch (error) {
1301
+ failures.push({ name: agentFolderFailureName(agent), error: `Could not read agent skills directory (${error.message})` });
924
1302
  }
925
1303
  return removed;
926
1304
  }
@@ -965,7 +1343,7 @@ async function detectDeletedAgentSymlinks(cloudSkills, previousState, skillsDir
965
1343
  continue;
966
1344
  }
967
1345
  const previous = previousState.skills[skill.name];
968
- if (!previous) {
1346
+ if (!previous || previous.cloudId !== skill.id || previous.verifiedAgentLinks?.[agent] !== true || !skill.updated_at || previous.cloudUpdatedAt !== skill.updated_at) {
969
1347
  continue;
970
1348
  }
971
1349
  const cloudTargets = normalizeAgentTargets(skill.agent_targets);
@@ -996,7 +1374,9 @@ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, option
996
1374
  const result = {
997
1375
  linked: 0,
998
1376
  removed: 0,
999
- skipped: 0
1377
+ skipped: 0,
1378
+ verifiedAgentLinks: {},
1379
+ failures: []
1000
1380
  };
1001
1381
  const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
1002
1382
  const legacyGlobalSkillsDir = options.legacyGlobalSkillsDir || LEGACY_AGENTS_SKILLS_DIR;
@@ -1031,21 +1411,34 @@ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, option
1031
1411
  result.skipped += desiredByAgent[agent].size;
1032
1412
  continue;
1033
1413
  }
1034
- await fs2.mkdir(agentDir, { recursive: true });
1414
+ try {
1415
+ await fs2.mkdir(agentDir, { recursive: true });
1416
+ } catch (error) {
1417
+ result.failures.push({ name: agentFolderFailureName(agent), error: `Could not create agent skills directory (${error.message})` });
1418
+ continue;
1419
+ }
1035
1420
  const desiredSkills = desiredByAgent[agent];
1036
1421
  if (options.removeUndesired !== false) {
1037
- result.removed += await removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots);
1422
+ result.removed += await removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots, result.failures, agent);
1038
1423
  }
1039
1424
  for (const skillName of desiredSkills) {
1040
1425
  const targetPath = path2.join(skillsDir, skillName);
1041
1426
  const linkPath = path2.join(agentDir, safeName(skillName, agentDir));
1042
1427
  try {
1043
- await fs2.access(targetPath);
1428
+ if (!(await fs2.stat(path2.join(targetPath, "SKILL.md"))).isFile()) throw new Error("Missing SKILL.md");
1044
1429
  } catch {
1045
1430
  result.skipped += 1;
1431
+ result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: SKILL.md is missing or unreadable` });
1432
+ continue;
1433
+ }
1434
+ let syncOutcome;
1435
+ try {
1436
+ syncOutcome = await ensureCorrectSymlink(linkPath, targetPath);
1437
+ } catch (error) {
1438
+ result.skipped += 1;
1439
+ result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: could not create skill link (${error.message})` });
1046
1440
  continue;
1047
1441
  }
1048
- const syncOutcome = await ensureCorrectSymlink(linkPath, targetPath);
1049
1442
  if (syncOutcome === "linked") {
1050
1443
  result.linked += 1;
1051
1444
  } else if (syncOutcome === "blocked") {
@@ -1053,9 +1446,13 @@ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, option
1053
1446
  `[skill-sync] Could not link "${skillName}" for ${agent}: non-symlink entry blocks ${linkPath}`
1054
1447
  );
1055
1448
  result.skipped += 1;
1449
+ result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: an existing file or folder blocks the skill link` });
1056
1450
  } else {
1057
1451
  result.skipped += 1;
1058
1452
  }
1453
+ if (syncOutcome !== "blocked") {
1454
+ result.verifiedAgentLinks[skillName] = { ...result.verifiedAgentLinks[skillName], [agent]: true };
1455
+ }
1059
1456
  }
1060
1457
  }
1061
1458
  if (options.removeUndesired !== false && !Object.values(agentSkillDirs).some(
@@ -1064,26 +1461,46 @@ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, option
1064
1461
  result.removed += await removeUndesiredManagedSymlinks(
1065
1462
  legacyGlobalSkillsDir,
1066
1463
  /* @__PURE__ */ new Set(),
1067
- managedRoots
1464
+ managedRoots,
1465
+ result.failures,
1466
+ "legacy"
1068
1467
  );
1069
1468
  }
1070
1469
  return result;
1071
1470
  }
1072
1471
 
1073
1472
  // src/runtime/skill-sync/sync-plan.ts
1074
- function getPushCandidates(localSkills, syncState, cloudCuratedSkillNames = /* @__PURE__ */ new Set(), cloudSkillNames) {
1473
+ function selectCloudSkillsToApply(skills) {
1474
+ const winners = /* @__PURE__ */ new Map();
1475
+ for (const skill of skills) {
1476
+ const current = winners.get(skill.name);
1477
+ if (!current || (skill.updated_at || "") >= (current.updated_at || "")) {
1478
+ winners.set(skill.name, skill);
1479
+ }
1480
+ }
1481
+ return [...winners.values()];
1482
+ }
1483
+ function getPushCandidates(localSkills, syncState, cloudCuratedSkillNames = /* @__PURE__ */ new Set(), cloudSkillNames, cloudSkills = []) {
1075
1484
  return localSkills.filter((skill) => {
1076
1485
  if (cloudCuratedSkillNames.has(skill.name)) {
1077
1486
  return false;
1078
1487
  }
1079
- const previous = syncState.skills[skill.name];
1488
+ const previous = syncState.skills[skill.name] || (skill.cloudId ? Object.values(syncState.skills).find((row) => row.cloudId === skill.cloudId) : void 0);
1489
+ const native = cloudSkills.find((row) => isNativeSkill(row) && row.id === (skill.cloudId || previous?.cloudId));
1490
+ if (native || skill.nativeReference || previous?.nativeReference || previous?.nativeUnavailable) {
1491
+ return Boolean(native && skill.nativeReference && skill.nativeScope === native.native_scope && (previous ? previous.folderHash !== skill.folderHash : skill.nativeBaseFolderHash !== skill.folderHash));
1492
+ }
1080
1493
  if (!previous) {
1494
+ if (hasNativeFolderHistory(syncState, skill.name) || /^native-[a-f0-9-]{36}$/.test(skill.name)) return false;
1081
1495
  return true;
1082
1496
  }
1083
1497
  if (cloudSkillNames && !cloudSkillNames.has(skill.name)) {
1084
1498
  return false;
1085
1499
  }
1086
1500
  return previous.folderHash !== skill.folderHash;
1501
+ }).map((skill) => {
1502
+ const previous = syncState.skills[skill.name] || Object.values(syncState.skills).find((row) => row.cloudId === skill.cloudId);
1503
+ return { ...skill, ...skill.cloudId || previous?.cloudId ? { cloudId: skill.cloudId || previous.cloudId } : {} };
1087
1504
  });
1088
1505
  }
1089
1506
 
@@ -1094,7 +1511,7 @@ async function writeCloudSkillWithBundleFallback(skill, dependencies) {
1094
1511
  const bundleBytes = await dependencies.downloadSkillBundle(skill.skill_source_url);
1095
1512
  const wroteBundleToDisk = await dependencies.writeCloudSkillToDisk(skill, bundleBytes);
1096
1513
  if (wroteBundleToDisk) {
1097
- return true;
1514
+ return wroteBundleToDisk;
1098
1515
  }
1099
1516
  dependencies.onWarning?.(
1100
1517
  `Bundle sync for "${skill.name}" produced no local changes, falling back to SKILL.md payload.`,
@@ -1126,7 +1543,7 @@ async function writeCloudSkillWithBundleFallback(skill, dependencies) {
1126
1543
  }
1127
1544
 
1128
1545
  // src/runtime/skill-sync/index.ts
1129
- var BASE_SKILL_NAMES = /* @__PURE__ */ new Set(["notis-apps", "notis-query", "notis-cli"]);
1546
+ var BASE_SKILL_NAMES = /* @__PURE__ */ new Set(["notis-spaces", "notis-query", "notis-cli"]);
1130
1547
  function withoutBaseSkills(pullResponse) {
1131
1548
  return {
1132
1549
  ...pullResponse,
@@ -1159,6 +1576,62 @@ var DEFAULT_RUN_SKILL_SYNC_DEPS = {
1159
1576
  removeForeignAccountSymlinks,
1160
1577
  updateAgentTargets
1161
1578
  };
1579
+ var MIRROR_ONLY_DEPS = {
1580
+ removeForeignAccountSymlinks: async () => 0,
1581
+ removeAllSymlinksForSkill: async () => 0,
1582
+ syncSymlinks: async () => ({ linked: 0, removed: 0, skipped: 0, verifiedAgentLinks: {} })
1583
+ };
1584
+ async function forgetOtherAccountsAgentLinks(paths) {
1585
+ const usersDir = dirname(dirname(paths.syncStatePath));
1586
+ let entries;
1587
+ try {
1588
+ entries = await fs3.readdir(usersDir, { withFileTypes: true });
1589
+ } catch {
1590
+ return;
1591
+ }
1592
+ for (const entry of entries) {
1593
+ if (!entry.isDirectory()) continue;
1594
+ const statePath = join(usersDir, entry.name, ".notis-sync.json");
1595
+ if (statePath === paths.syncStatePath) continue;
1596
+ let state = null;
1597
+ try {
1598
+ state = JSON.parse(await fs3.readFile(statePath, "utf8"));
1599
+ } catch {
1600
+ continue;
1601
+ }
1602
+ if (!state || typeof state !== "object" || !state.skills || typeof state.skills !== "object") continue;
1603
+ let changed = false;
1604
+ for (const skill of Object.values(state.skills)) {
1605
+ if (skill && typeof skill === "object" && "verifiedAgentLinks" in skill) {
1606
+ delete skill.verifiedAgentLinks;
1607
+ changed = true;
1608
+ }
1609
+ }
1610
+ if (!changed) continue;
1611
+ const temporaryPath = `${statePath}.${process.pid}.${Date.now()}.tmp`;
1612
+ await fs3.writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}
1613
+ `, "utf8");
1614
+ await fs3.rename(temporaryPath, statePath);
1615
+ }
1616
+ }
1617
+ function isolatedLinkOptions(paths) {
1618
+ if (!paths.isolatedNamespace && !paths.homeDir) return void 0;
1619
+ const root = paths.isolatedNamespace ? join(paths.syncRoot, "environments", paths.isolatedNamespace, "agents") : paths.homeDir;
1620
+ return {
1621
+ agentSkillDirs: {
1622
+ notis: paths.skillsDir,
1623
+ claude_code: join(root, ".claude", "skills"),
1624
+ cursor: join(root, ".cursor", "skills"),
1625
+ codex: join(root, ".codex", "skills")
1626
+ },
1627
+ legacyGlobalSkillsDir: join(root, ".agents", "skills")
1628
+ };
1629
+ }
1630
+ function assertSyncEnvironment(pull, settings) {
1631
+ if ((pull.sync_namespace || null) !== (settings.sync_namespace || null) || settings.sync_scope !== void 0 && pull.sync_scope !== settings.sync_scope) {
1632
+ throw new Error("Skill sync environment changed. Existing mirrors were retained.");
1633
+ }
1634
+ }
1162
1635
  function toSkillMap(skills) {
1163
1636
  return new Map(skills.map((skill) => [skill.name, skill]));
1164
1637
  }
@@ -1174,6 +1647,15 @@ function decodeJwtSubject(jwt) {
1174
1647
  return null;
1175
1648
  }
1176
1649
  }
1650
+ function cloudContentHash(skill) {
1651
+ return createHash4("sha256").update(JSON.stringify({
1652
+ md: skill.skill_md,
1653
+ hash: skill.skill_folder_hash,
1654
+ source: skill.skill_source_url,
1655
+ files: skill.bundle_files?.slice().sort((a, b) => a.path.localeCompare(b.path)),
1656
+ hydrationFailed: skill.bundle_hydration_failed === true
1657
+ })).digest("hex");
1658
+ }
1177
1659
  function shouldWriteCloudSkill(cloudSkill, localSkills, previousState) {
1178
1660
  const skillName = cloudSkill.name;
1179
1661
  const cloudHash = cloudSkill.skill_folder_hash || "";
@@ -1181,40 +1663,99 @@ function shouldWriteCloudSkill(cloudSkill, localSkills, previousState) {
1181
1663
  if (!localSkill) {
1182
1664
  return true;
1183
1665
  }
1666
+ if (isNativeSkill(cloudSkill)) {
1667
+ if (localSkill.nativeScope && localSkill.nativeScope !== cloudSkill.native_scope) return false;
1668
+ if (nativeLocalReady(cloudSkill, localSkill)) return false;
1669
+ const nativePrevious = previousNativeState(previousState, cloudSkill);
1670
+ return localSkill.nativeFilesCurrent === true && localSkill.folderHash === localSkill.nativeBaseFolderHash || !localSkill.nativeReference && nativePrevious?.cloudId === cloudSkill.id && nativePrevious.folderHash === localSkill.folderHash;
1671
+ }
1672
+ const previous = previousState.skills[skillName];
1673
+ if (previous?.folderHash === localSkill.folderHash && previous.cloudContentHash === cloudContentHash(cloudSkill)) return false;
1674
+ if (previous && previous.cloudContentHash === void 0 && previous.cloudId === cloudSkill.id && previous.appliedCloudFolderHash !== void 0 && previous.appliedCloudFolderHash === cloudHash && previous.cloudUpdatedAt === cloudSkill.updated_at && previous.folderHash === localSkill.folderHash) {
1675
+ return false;
1676
+ }
1184
1677
  if (cloudSkill.source === "curated") {
1185
1678
  return cloudHash ? cloudHash !== localSkill.folderHash : true;
1186
1679
  }
1187
- const previous = previousState.skills[skillName];
1188
1680
  const localChangedSinceLastSync = !previous || previous.folderHash !== localSkill.folderHash;
1189
- return !localChangedSinceLastSync && Boolean(cloudHash) && cloudHash !== localSkill.folderHash;
1681
+ return !localChangedSinceLastSync && (Boolean(cloudHash) && cloudHash !== localSkill.folderHash || Boolean(previous?.cloudContentHash && previous.cloudContentHash !== cloudContentHash(cloudSkill)));
1682
+ }
1683
+ function collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames) {
1684
+ const applied = {};
1685
+ for (const skill of selectCloudSkillsToApply(pullResponse.skills)) {
1686
+ applied[skill.name] = writtenSkillNames.has(skill.name) ? skill.skill_folder_hash || "" : previousState.skills[skill.name]?.appliedCloudFolderHash;
1687
+ }
1688
+ return applied;
1689
+ }
1690
+ function collectAppliedFiles(pullResponse, previousState, appliedFilesByName) {
1691
+ const applied = {};
1692
+ for (const skill of selectCloudSkillsToApply(pullResponse.skills)) {
1693
+ applied[skill.name] = appliedFilesByName[skill.name] ?? previousState.skills[skill.name]?.appliedFiles;
1694
+ }
1695
+ return applied;
1190
1696
  }
1191
- function buildSyncState(pullResponse, localSkills, lastSyncedAt) {
1697
+ function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLinks = {}, failedContentNames = /* @__PURE__ */ new Set(), appliedRevisions = {}, appliedFiles = {}, previousState = { version: 1, lastSyncedAt: null, skills: {} }) {
1192
1698
  const localSkillMap = toSkillMap(localSkills);
1193
1699
  const skills = Object.fromEntries(
1194
- pullResponse.skills.map((skill) => {
1700
+ selectCloudSkillsToApply(pullResponse.skills).map((skill) => {
1195
1701
  const localSkill = localSkillMap.get(skill.name);
1702
+ const appliedCloudFolderHash = appliedRevisions[skill.name];
1703
+ const appliedFileList = appliedFiles[skill.name];
1704
+ if (pendingLegacyUpgrade(skill, localSkill, previousState)) {
1705
+ const { nativeUnavailable: _unavailable, ...previous } = previousNativeState(previousState, skill);
1706
+ return [skill.name, { ...previous, verifiedAgentLinks: verifiedAgentLinks[skill.name] ?? {} }];
1707
+ }
1708
+ if (isNativeSkill(skill) && !nativeLocalReady(skill, localSkill)) {
1709
+ const previous = previousNativeState(previousState, skill);
1710
+ return [skill.name, { ...previous || {
1711
+ cloudId: skill.id,
1712
+ folderHash: localSkill?.nativeBaseFolderHash || "",
1713
+ agentTargets: normalizeAgentTargets(skill.agent_targets),
1714
+ syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString(),
1715
+ ...localSkill?.nativeReference ? { nativeReference: localSkill.nativeReference, nativeScope: localSkill.nativeScope } : {}
1716
+ }, nativeUnavailable: true, verifiedAgentLinks: {} }];
1717
+ }
1196
1718
  return [
1197
1719
  skill.name,
1198
1720
  {
1199
1721
  cloudId: skill.id,
1722
+ ...nativeState(skill),
1200
1723
  folderHash: localSkill?.folderHash || skill.skill_folder_hash || "",
1201
1724
  agentTargets: normalizeAgentTargets(skill.agent_targets),
1725
+ verifiedAgentLinks: skill.status === "active" ? verifiedAgentLinks[skill.name] ?? {} : {},
1726
+ cloudUpdatedAt: skill.updated_at,
1727
+ ...!failedContentNames.has(skill.name) && !skill.skill_source_url ? { cloudContentHash: cloudContentHash(skill) } : {},
1728
+ ...appliedCloudFolderHash !== void 0 ? { appliedCloudFolderHash } : {},
1729
+ ...appliedFileList !== void 0 ? { appliedFiles: appliedFileList } : {},
1202
1730
  syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString()
1203
1731
  }
1204
1732
  ];
1205
1733
  })
1206
1734
  );
1207
- return {
1735
+ for (const local of localSkills) {
1736
+ if (!local.nativeReference || Object.values(skills).some((row) => row.cloudId === local.cloudId)) continue;
1737
+ skills[local.name] = {
1738
+ cloudId: local.cloudId,
1739
+ nativeReference: local.nativeReference,
1740
+ nativeScope: local.nativeScope,
1741
+ nativeUnavailable: true,
1742
+ verifiedAgentLinks: {},
1743
+ folderHash: local.nativeBaseFolderHash || "",
1744
+ agentTargets: normalizeAgentTargets(),
1745
+ syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString()
1746
+ };
1747
+ }
1748
+ return retainMissingNativeState(previousState, {
1208
1749
  version: 1,
1209
1750
  lastSyncedAt,
1210
1751
  skills
1211
- };
1752
+ });
1212
1753
  }
1213
1754
  function buildLocalSymlinkCandidates(pullResponse, localSkills, previousState) {
1214
1755
  const cloudSkillNames = new Set(
1215
1756
  pullResponse.skills.map((skill) => skill.name)
1216
1757
  );
1217
- const localOnlySkills = localSkills.filter((skill) => !cloudSkillNames.has(skill.name)).map((skill) => {
1758
+ const localOnlySkills = localSkills.filter((skill) => !cloudSkillNames.has(skill.name)).filter((skill) => !skill.nativeReference && !previousState.skills[skill.name]?.nativeReference && !previousState.skills[skill.name]?.nativeUnavailable && !hasNativeFolderHistory(previousState, skill.name) && !/^native-[a-f0-9-]{36}$/.test(skill.name)).map((skill) => {
1218
1759
  const previous = previousState.skills[skill.name];
1219
1760
  return {
1220
1761
  id: previous?.cloudId || `local-${skill.name}`,
@@ -1227,7 +1768,7 @@ function buildLocalSymlinkCandidates(pullResponse, localSkills, previousState) {
1227
1768
  status: "active"
1228
1769
  };
1229
1770
  });
1230
- return [...pullResponse.skills, ...localOnlySkills];
1771
+ return [...nativeLinkRows(pullResponse, localSkills, previousState), ...localOnlySkills];
1231
1772
  }
1232
1773
  function isEmptySyncState(state) {
1233
1774
  return state.lastSyncedAt === null && Object.keys(state.skills).length === 0;
@@ -1254,27 +1795,48 @@ function applyLegacyFirstRunState(localSkills, scopedState, legacyState) {
1254
1795
  skills: migratedSkills
1255
1796
  };
1256
1797
  }
1257
- async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previousState, syncPaths, deps) {
1798
+ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previousState, syncPaths, deps, failures = [], writtenSkillNames = /* @__PURE__ */ new Set(), appliedFilesByName = {}, acknowledgedNative = /* @__PURE__ */ new Map()) {
1258
1799
  const localSkillMap = toSkillMap(localSkills);
1259
1800
  const warnSkillSync = (message, error) => {
1260
1801
  console.warn(`[Notis] ${message}`, error);
1261
1802
  };
1262
1803
  let downloaded = 0;
1263
- for (const cloudSkill of pullResponse.skills) {
1264
- if (!shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
1804
+ for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
1805
+ const local = localSkillMap.get(cloudSkill.name);
1806
+ const acknowledged = isNativeSkill(cloudSkill) && local && acknowledgedNative.get(cloudSkill.id) === local.folderHash;
1807
+ const legacyBaseline = Boolean(local && pendingLegacyUpgrade(cloudSkill, local, previousState) && previousNativeState(previousState, cloudSkill)?.folderHash === local.folderHash);
1808
+ if (!acknowledged && !shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
1809
+ if (isNativeSkill(cloudSkill) && !nativeLocalReady(cloudSkill, local)) {
1810
+ failures.push({ name: cloudSkill.name, error: "Local native Skill changes were retained; resolve the version or identity conflict before using them." });
1811
+ }
1265
1812
  continue;
1266
1813
  }
1267
- if (await writeCloudSkillWithBundleFallback(cloudSkill, {
1814
+ const outcome = await writeCloudSkillWithBundleFallback(cloudSkill, {
1268
1815
  downloadSkillBundle: deps.downloadSkillBundle,
1269
- writeCloudSkillToDisk: (skill, bundleBytes) => deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths),
1816
+ writeCloudSkillToDisk: (skill, bundleBytes) => deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths, {
1817
+ // What the last write put there. Everything else in the folder was
1818
+ // produced locally and survives this one.
1819
+ previouslyApplied: previousNativeState(previousState, skill)?.appliedFiles,
1820
+ ...isNativeSkill(skill) && local ? { expectedFolderHash: local.folderHash } : {},
1821
+ ...legacyBaseline ? { legacyBaseline } : {}
1822
+ }),
1270
1823
  onWarning: warnSkillSync
1271
- })) {
1824
+ });
1825
+ if (outcome) {
1272
1826
  downloaded += 1;
1827
+ writtenSkillNames.add(cloudSkill.name);
1828
+ if (typeof outcome === "object" && Array.isArray(outcome.appliedFiles)) {
1829
+ appliedFilesByName[cloudSkill.name] = outcome.appliedFiles;
1830
+ }
1831
+ } else {
1832
+ failures.push({ name: cloudSkill.name, error: "Skill content could not be downloaded or written; sync will retry" });
1273
1833
  }
1274
1834
  }
1275
1835
  return downloaded;
1276
1836
  }
1277
1837
  function assertSkillsPullAuthorized(pullResponse) {
1838
+ if (!Array.isArray(pullResponse.skills)) throw new Error("Skill inventory is unavailable; existing files were retained.");
1839
+ assertNativePull(pullResponse);
1278
1840
  if (pullResponse.entitlement_access?.code === "entitlement_upgrade_required" && pullResponse.entitlement_access.entitlement === "skills") {
1279
1841
  throw new Error(
1280
1842
  "Skill sync access was denied; preserving existing local skills."
@@ -1292,68 +1854,108 @@ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies
1292
1854
  "Cannot materialize skills without a valid authenticated desktop session."
1293
1855
  );
1294
1856
  }
1295
- const syncPaths = getSkillSyncPathsForUser(authUserId);
1296
- const pullResponse = await deps.pullSkills(serverUrl, jwt);
1857
+ let pullResponse = await deps.pullSkills(serverUrl, jwt);
1297
1858
  assertSkillsPullAuthorized(pullResponse);
1859
+ if (options.syncSettings) assertSyncEnvironment(pullResponse, options.syncSettings);
1860
+ const syncPaths = getSkillSyncPathsForUser(options.canonicalUserId?.trim() || authUserId, void 0, pullResponse.sync_namespace);
1298
1861
  const previousState = await deps.readSyncState(syncPaths);
1299
1862
  const localSkills = await deps.scanLocalSkills(syncPaths);
1863
+ pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
1864
+ const failedDownloads = [];
1865
+ const writtenSkillNames = /* @__PURE__ */ new Set();
1866
+ const appliedFilesByName = {};
1300
1867
  const downloaded = await writePulledSkillsToScopedMirror(
1301
1868
  pullResponse,
1302
1869
  localSkills,
1303
1870
  previousState,
1304
1871
  syncPaths,
1305
- deps
1872
+ deps,
1873
+ failedDownloads,
1874
+ writtenSkillNames,
1875
+ appliedFilesByName
1306
1876
  );
1307
1877
  const finalLocalSkills = await deps.scanLocalSkills(syncPaths);
1308
1878
  const lastSyncedAt = pullResponse.last_synced_at || (/* @__PURE__ */ new Date()).toISOString();
1309
1879
  const relinkSkillNames = new Set(options.relinkSkillNames || []);
1880
+ const failures = [...failedDownloads];
1881
+ const verifiedLinks = {};
1882
+ for (const skill of pullResponse.skills) {
1883
+ const previous = previousState.skills[skill.name];
1884
+ if (skill.updated_at && previous?.cloudId === skill.id && previous.cloudUpdatedAt === skill.updated_at) {
1885
+ verifiedLinks[skill.name] = { ...previous.verifiedAgentLinks };
1886
+ }
1887
+ }
1310
1888
  if (relinkSkillNames.size > 0) {
1311
- await deps.syncSymlinks(
1312
- pullResponse.skills.filter((skill) => relinkSkillNames.has(skill.name)),
1889
+ const relinked = await deps.syncSymlinks(
1890
+ nativeLinkRows(pullResponse, finalLocalSkills, previousState).filter((skill) => relinkSkillNames.has(skill.name)),
1313
1891
  syncPaths.skillsDir,
1314
- { removeUndesired: false }
1892
+ { ...isolatedLinkOptions(syncPaths), removeUndesired: false }
1315
1893
  );
1894
+ failures.push(...(relinked.failures ?? []).filter(
1895
+ (failure) => !failedDownloads.some((download) => download.name === failure.name)
1896
+ ));
1897
+ for (const name of relinkSkillNames) {
1898
+ verifiedLinks[name] = relinked.verifiedAgentLinks?.[name] ?? {};
1899
+ }
1316
1900
  }
1317
- await deps.writeSyncState(
1318
- buildSyncState(pullResponse, finalLocalSkills, lastSyncedAt),
1319
- syncPaths
1901
+ for (const failure of failedDownloads) delete verifiedLinks[failure.name];
1902
+ const materializedState = buildSyncState(
1903
+ pullResponse,
1904
+ finalLocalSkills,
1905
+ lastSyncedAt,
1906
+ verifiedLinks,
1907
+ new Set(failedDownloads.map((item) => item.name)),
1908
+ collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
1909
+ collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
1910
+ previousState
1320
1911
  );
1912
+ for (const [name, entry] of Object.entries(materializedState.skills)) {
1913
+ if (writtenSkillNames.has(name)) continue;
1914
+ const previous = previousState.skills[name];
1915
+ if (previous) {
1916
+ entry.folderHash = previous.folderHash;
1917
+ entry.cloudContentHash = previous.cloudContentHash;
1918
+ } else {
1919
+ delete materializedState.skills[name];
1920
+ }
1921
+ }
1922
+ materializedState.skills = { ...previousState.skills, ...materializedState.skills };
1923
+ let removed = 0;
1924
+ for (const [name, previous] of Object.entries(previousState.skills)) {
1925
+ if (!previous.nativeReference && !previous.nativeUnavailable) continue;
1926
+ const current = pullResponse.skills.find((skill) => skill.id === previous.cloudId);
1927
+ const local = current && finalLocalSkills.find((row) => row.name === current.name);
1928
+ if (!current || !nativeLocalReady(current, local) && !pendingLegacyUpgrade(current, local, previousState)) {
1929
+ removed += await deps.removeAllSymlinksForSkill(name, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
1930
+ }
1931
+ }
1932
+ await deps.writeSyncState(materializedState, syncPaths);
1321
1933
  return {
1934
+ materializedSkillNames: pullResponse.skills.filter((skill) => finalLocalSkills.some((local) => local.name === skill.name) && (!isNativeSkill(skill) || skill.status === "active" && skill.agent_targets?.notis !== false && nativeLocalReady(skill, finalLocalSkills.find((local) => local.name === skill.name))) && !failedDownloads.some((failure) => failure.name === skill.name)).map((skill) => skill.name),
1322
1935
  pulled: pullResponse.skills.length,
1323
1936
  downloaded,
1324
1937
  deleted: 0,
1325
- removed: 0,
1326
- lastSyncedAt
1938
+ removed,
1939
+ lastSyncedAt,
1940
+ ...failures.length ? { failedLinks: failures } : {}
1327
1941
  };
1328
1942
  }
1329
- async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previousState, scopedState, skillsDir, deps) {
1943
+ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previousState, scopedState, skillsDir, deps, failures, linkOptions) {
1330
1944
  if (isEmptySyncState(scopedState)) {
1331
1945
  return 0;
1332
1946
  }
1333
1947
  const deletions = await deps.detectDeletedAgentSymlinks(
1334
- pullResponse.skills,
1948
+ pullResponse.skills.map((skill) => {
1949
+ const previous = isNativeSkill(skill) ? Object.entries(previousState.skills).find(([, row]) => row.cloudId === skill.id) : void 0;
1950
+ return previous ? { ...skill, name: previous[0] } : skill;
1951
+ }),
1335
1952
  previousState,
1336
- skillsDir
1953
+ skillsDir,
1954
+ linkOptions
1337
1955
  );
1338
1956
  if (deletions.length === 0) {
1339
1957
  return 0;
1340
1958
  }
1341
- const latestSkillsById = new Map(pullResponse.skills.map((skill) => [skill.id, skill]));
1342
- let fresh = null;
1343
- try {
1344
- fresh = await deps.pullSkills(serverUrl, jwt);
1345
- } catch (error) {
1346
- console.warn(
1347
- "[skill-sync] Could not re-pull latest agent targets before deactivation; using the top-of-sync snapshot.",
1348
- error
1349
- );
1350
- }
1351
- if (fresh) {
1352
- assertSkillsPullAuthorized(fresh);
1353
- for (const skill of fresh.skills) {
1354
- latestSkillsById.set(skill.id, skill);
1355
- }
1356
- }
1357
1959
  const agentsBySkill = /* @__PURE__ */ new Map();
1358
1960
  for (const deletion of deletions) {
1359
1961
  const entry = agentsBySkill.get(deletion.skillId) ?? {
@@ -1363,37 +1965,57 @@ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previo
1363
1965
  entry.agents.add(deletion.agent);
1364
1966
  agentsBySkill.set(deletion.skillId, entry);
1365
1967
  }
1366
- const inMemoryById = new Map(pullResponse.skills.map((skill) => [skill.id, skill]));
1968
+ const fresh = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1969
+ assertSkillsPullAuthorized(fresh);
1970
+ assertSyncEnvironment(fresh, pullResponse);
1971
+ Object.assign(pullResponse, fresh);
1972
+ let needsRefresh = false;
1367
1973
  let deactivated = 0;
1368
1974
  for (const [skillId, { skillName, agents }] of agentsBySkill) {
1369
- const latest = latestSkillsById.get(skillId);
1370
- if (!latest) {
1371
- continue;
1372
- }
1373
- const nextTargets = { ...normalizeAgentTargets(latest.agent_targets) };
1374
- for (const agent of agents) {
1375
- nextTargets[agent] = false;
1376
- }
1975
+ const skill = pullResponse.skills.find((item) => item.id === skillId);
1976
+ const previous = previousState.skills[skillName];
1977
+ if (!skill) continue;
1978
+ const native = isNativeSkill(skill);
1979
+ if (native) {
1980
+ if (!skill.native_settings || previous?.nativeSettingsRevision !== skill.native_settings.revision) continue;
1981
+ } else if (!skill.updated_at || previous?.cloudUpdatedAt !== skill.updated_at) continue;
1982
+ const patch = Object.fromEntries([...agents].map((agent) => [agent, false]));
1377
1983
  try {
1378
- await deps.updateAgentTargets(serverUrl, jwt, skillId, nextTargets);
1379
- const inMemory = inMemoryById.get(skillId);
1380
- if (inMemory) {
1381
- inMemory.agent_targets = nextTargets;
1984
+ const saved = await deps.updateAgentTargets(
1985
+ serverUrl,
1986
+ jwt,
1987
+ skillId,
1988
+ patch,
1989
+ native ? void 0 : skill.updated_at,
1990
+ native ? { reference: skill.native_reference, settingsRevision: skill.native_settings.revision } : void 0
1991
+ );
1992
+ if (saved.success !== true || !saved.updated_at?.trim() || !native && saved.updated_at === skill.updated_at || !["notis", "claude_code", "cursor", "codex"].every((agent) => typeof saved.agent_targets?.[agent] === "boolean") || ![...agents].every((agent) => saved.agent_targets[agent] === false)) {
1993
+ throw new Error("Assignment update did not return a verified saved revision");
1382
1994
  }
1995
+ skill.agent_targets = saved.agent_targets;
1996
+ skill.updated_at = saved.updated_at;
1997
+ if (native) needsRefresh = true;
1383
1998
  deactivated += agents.size;
1384
1999
  } catch (error) {
1385
- console.warn(
1386
- `[skill-sync] Failed to deactivate "${skillName}" for ${[...agents].join(", ")} after local symlink deletion:`,
1387
- error
1388
- );
2000
+ needsRefresh = true;
2001
+ failures.push({ name: skillName, error: "Could not save the local agent removal; refreshed saved assignments" });
2002
+ console.warn(`[skill-sync] Assignment changed or could not be saved for "${skillName}"; refreshing before reconciliation.`, error);
1389
2003
  }
1390
2004
  }
2005
+ if (needsRefresh) {
2006
+ const refreshed = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
2007
+ assertSkillsPullAuthorized(refreshed);
2008
+ assertSyncEnvironment(refreshed, pullResponse);
2009
+ Object.assign(pullResponse, refreshed);
2010
+ }
1391
2011
  return deactivated;
1392
2012
  }
1393
2013
  async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
2014
+ const linkAgentFolders = options.linkAgentFolders !== false;
1394
2015
  const deps = {
1395
2016
  ...DEFAULT_RUN_SKILL_SYNC_DEPS,
1396
- ...dependencies
2017
+ ...dependencies,
2018
+ ...linkAgentFolders ? {} : MIRROR_ONLY_DEPS
1397
2019
  };
1398
2020
  const syncSettings = await deps.fetchSyncSettings(
1399
2021
  serverUrl,
@@ -1405,11 +2027,12 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1405
2027
  "Cannot sync skills without a server-verified account identity."
1406
2028
  );
1407
2029
  }
1408
- const syncPaths = getSkillSyncPathsForUser(syncUserId);
1409
- const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
1410
- syncPaths.skillsDir
1411
- );
2030
+ const syncPaths = getSkillSyncPathsForUser(syncUserId, void 0, syncSettings.sync_namespace, options.syncHome);
1412
2031
  if (options.honorSyncEnabled !== false && !syncSettings.sync_enabled) {
2032
+ const foreignLinksRemoved2 = await deps.removeForeignAccountSymlinks(
2033
+ syncPaths.skillsDir,
2034
+ isolatedLinkOptions(syncPaths)
2035
+ );
1413
2036
  return {
1414
2037
  syncEnabled: false,
1415
2038
  pushed: 0,
@@ -1418,7 +2041,7 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1418
2041
  deleted: 0,
1419
2042
  deactivated: 0,
1420
2043
  linked: 0,
1421
- removed: foreignLinksRemoved,
2044
+ removed: foreignLinksRemoved2,
1422
2045
  skipped: 0,
1423
2046
  lastSyncedAt: syncSettings.last_synced_at,
1424
2047
  failedPushes: []
@@ -1426,52 +2049,77 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1426
2049
  }
1427
2050
  let pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1428
2051
  assertSkillsPullAuthorized(pullResponse);
2052
+ assertSyncEnvironment(pullResponse, syncSettings);
2053
+ const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
2054
+ syncPaths.skillsDir,
2055
+ isolatedLinkOptions(syncPaths)
2056
+ );
2057
+ if (linkAgentFolders && !syncPaths.isolatedNamespace) {
2058
+ await forgetOtherAccountsAgentLinks(syncPaths);
2059
+ }
1429
2060
  const cloudCuratedSkillNames = new Set(
1430
2061
  pullResponse.skills.filter((skill) => skill.source === "curated").map((skill) => skill.name)
1431
2062
  );
1432
2063
  const protectedSkillNames = /* @__PURE__ */ new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
2064
+ const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
2065
+ const assignmentFailures = [];
2066
+ const deactivated = linkAgentFolders && !syncPaths.isolatedNamespace && syncSettings.agent_targets_conditional_updates === true ? await deactivateDeletedAgentSkills(
2067
+ serverUrl,
2068
+ jwt,
2069
+ pullResponse,
2070
+ scopedState,
2071
+ scopedState,
2072
+ syncPaths.skillsDir,
2073
+ deps,
2074
+ assignmentFailures,
2075
+ isolatedLinkOptions(syncPaths)
2076
+ ) : 0;
2077
+ assertSyncEnvironment(pullResponse, syncSettings);
1433
2078
  const authUserId = decodeJwtSubject(jwt);
1434
2079
  let previousAuthState = null;
1435
- if (authUserId && authUserId !== syncUserId) {
1436
- const previousAuthPaths = getSkillSyncPathsForUser(authUserId);
2080
+ if (!syncPaths.isolatedNamespace && authUserId && authUserId !== syncUserId) {
2081
+ const previousAuthPaths = getSkillSyncPathsForUser(authUserId, void 0, void 0, options.syncHome);
1437
2082
  previousAuthState = await deps.readSyncState(previousAuthPaths);
1438
2083
  await deps.gatherTopLevelLocalSkills(syncPaths, {
1439
2084
  sourceRoots: [{ label: "previous-auth-scope", root: previousAuthPaths.skillsDir }],
1440
2085
  protectedSkillNames
1441
2086
  });
1442
2087
  }
1443
- await deps.gatherTopLevelLocalSkills(syncPaths, {
1444
- protectedSkillNames
1445
- });
2088
+ if (linkAgentFolders && !syncPaths.isolatedNamespace) {
2089
+ await deps.gatherTopLevelLocalSkills(syncPaths, { protectedSkillNames });
2090
+ }
1446
2091
  const localSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
1447
- const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
1448
2092
  const previousState = withoutBaseSkillState(applyLegacyFirstRunState(
1449
2093
  localSkills,
1450
2094
  scopedState,
1451
- isEmptySyncState(scopedState) ? !previousAuthState || isEmptySyncState(previousAuthState) ? await deps.readLegacySyncState(syncPaths) : previousAuthState : null
2095
+ !syncPaths.isolatedNamespace && isEmptySyncState(scopedState) ? !previousAuthState || isEmptySyncState(previousAuthState) ? await deps.readLegacySyncState(syncPaths) : previousAuthState : null
1452
2096
  ));
1453
- const deactivated = await deactivateDeletedAgentSkills(
1454
- serverUrl,
1455
- jwt,
1456
- pullResponse,
1457
- previousState,
1458
- scopedState,
1459
- syncPaths.skillsDir,
1460
- deps
1461
- );
1462
- await deps.syncSymlinks(
2097
+ pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
2098
+ const gatheredSymlinkResult = await deps.syncSymlinks(
1463
2099
  buildLocalSymlinkCandidates(pullResponse, localSkills, previousState),
1464
- syncPaths.skillsDir
2100
+ syncPaths.skillsDir,
2101
+ isolatedLinkOptions(syncPaths)
1465
2102
  );
1466
2103
  const pushCandidates = getPushCandidates(
1467
2104
  localSkills,
1468
2105
  previousState,
1469
2106
  cloudCuratedSkillNames,
1470
- new Set(pullResponse.skills.map((skill) => skill.name))
2107
+ new Set(pullResponse.skills.map((skill) => skill.name)),
2108
+ pullResponse.skills
1471
2109
  );
1472
2110
  const failedPushes = [];
2111
+ const acknowledgedNative = /* @__PURE__ */ new Map();
2112
+ let writeLocalSkills = localSkills;
1473
2113
  if (pushCandidates.length > 0) {
1474
2114
  const pushResult = await deps.pushChangedSkills(serverUrl, jwt, pushCandidates);
2115
+ for (const result of pushResult.skills || []) {
2116
+ const saved = result;
2117
+ const candidate = pushCandidates.find((local) => local.nativeReference && local.cloudId === saved.id);
2118
+ if (candidate && saved.native_reference) {
2119
+ checkedNativeReference(saved.native_reference, candidate.cloudId);
2120
+ acknowledgedNative.set(candidate.cloudId, candidate.folderHash);
2121
+ }
2122
+ }
1475
2123
  if (Array.isArray(pushResult?.failed) && pushResult.failed.length > 0) {
1476
2124
  failedPushes.push(...pushResult.failed);
1477
2125
  console.warn(
@@ -1480,31 +2128,58 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1480
2128
  }
1481
2129
  pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1482
2130
  assertSkillsPullAuthorized(pullResponse);
2131
+ assertSyncEnvironment(pullResponse, syncSettings);
2132
+ writeLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
2133
+ pullResponse = bindNativeFolders(pullResponse, writeLocalSkills, previousState);
1483
2134
  }
1484
2135
  const cloudSkillNames = new Set(pullResponse.skills.map((s) => s.name));
1485
2136
  let deleted = 0;
1486
2137
  for (const skillName of Object.keys(previousState.skills)) {
1487
2138
  if (!cloudSkillNames.has(skillName)) {
2139
+ const previous = previousState.skills[skillName];
2140
+ if (previous.nativeReference || previous.nativeUnavailable || pullResponse.skills.some((skill) => isNativeSkill(skill) && skill.id === previous.cloudId)) {
2141
+ await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
2142
+ continue;
2143
+ }
1488
2144
  await deps.deleteLocalSkill(skillName, syncPaths);
1489
- await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir);
2145
+ await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
1490
2146
  deleted += 1;
1491
2147
  }
1492
2148
  }
2149
+ const failedDownloads = [];
2150
+ const writtenSkillNames = /* @__PURE__ */ new Set();
2151
+ const appliedFilesByName = {};
1493
2152
  const downloaded = await writePulledSkillsToScopedMirror(
1494
2153
  pullResponse,
1495
- localSkills,
2154
+ writeLocalSkills,
1496
2155
  previousState,
1497
2156
  syncPaths,
1498
- deps
2157
+ deps,
2158
+ failedDownloads,
2159
+ writtenSkillNames,
2160
+ appliedFilesByName,
2161
+ acknowledgedNative
1499
2162
  );
1500
2163
  const finalLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
1501
2164
  const symlinkResult = await deps.syncSymlinks(
1502
2165
  buildLocalSymlinkCandidates(pullResponse, finalLocalSkills, previousState),
1503
- syncPaths.skillsDir
2166
+ syncPaths.skillsDir,
2167
+ isolatedLinkOptions(syncPaths)
1504
2168
  );
2169
+ const verifiedLinks = { ...symlinkResult.verifiedAgentLinks ?? {} };
2170
+ for (const failure of failedDownloads) delete verifiedLinks[failure.name];
1505
2171
  const lastSyncedAt = pullResponse.last_synced_at || (/* @__PURE__ */ new Date()).toISOString();
1506
2172
  await deps.writeSyncState(
1507
- buildSyncState(pullResponse, finalLocalSkills, lastSyncedAt),
2173
+ buildSyncState(
2174
+ pullResponse,
2175
+ finalLocalSkills,
2176
+ lastSyncedAt,
2177
+ verifiedLinks,
2178
+ new Set(failedDownloads.map((item) => item.name)),
2179
+ collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
2180
+ collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
2181
+ previousState
2182
+ ),
1508
2183
  syncPaths
1509
2184
  );
1510
2185
  return {
@@ -1514,15 +2189,21 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1514
2189
  downloaded,
1515
2190
  deleted,
1516
2191
  deactivated,
1517
- linked: symlinkResult.linked,
1518
- removed: foreignLinksRemoved + symlinkResult.removed,
2192
+ linked: gatheredSymlinkResult.linked + symlinkResult.linked,
2193
+ removed: foreignLinksRemoved + gatheredSymlinkResult.removed + symlinkResult.removed,
1519
2194
  skipped: symlinkResult.skipped,
2195
+ failedLinks: [...assignmentFailures, ...failedDownloads, ...(symlinkResult.failures ?? []).filter(
2196
+ (failure) => !failedDownloads.some((download) => download.name === failure.name)
2197
+ )],
1520
2198
  lastSyncedAt,
1521
2199
  failedPushes
1522
2200
  };
1523
2201
  }
1524
2202
  export {
2203
+ fetchSyncSettings,
2204
+ isolatedLinkOptions,
1525
2205
  materializeCloudSkillsForLocalShell,
1526
- runSkillSync
2206
+ runSkillSync,
2207
+ shouldWriteCloudSkill
1527
2208
  };
1528
2209
  //# sourceMappingURL=index.js.map