@notis_ai/cli 0.2.14 → 0.2.15

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 (296) hide show
  1. package/README.md +82 -20
  2. package/dist/agent-hooks/notis-agent-hook.mjs +19750 -0
  3. package/{skills → dist/base-skills}/notis-apps/SKILL.md +84 -53
  4. package/{skills → dist/base-skills}/notis-cli/SKILL.md +9 -9
  5. package/dist/base-skills/notis-query/SKILL.md +705 -0
  6. package/dist/skill-sync/index.js +1528 -0
  7. package/dist/skill-sync/index.js.map +7 -0
  8. package/package.json +5 -2
  9. package/skills/notis-apps/cli.md +38 -17
  10. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  11. package/skills/notis-onboarding/BRIEF.md +16 -0
  12. package/src/agent-hook-entry.js +5 -0
  13. package/src/cli.js +23 -14
  14. package/src/command-specs/agents.js +392 -0
  15. package/src/command-specs/apps.js +871 -387
  16. package/src/command-specs/auth.js +63 -10
  17. package/src/command-specs/index.js +6 -0
  18. package/src/command-specs/onboarding.js +69 -5
  19. package/src/command-specs/skills.js +56 -0
  20. package/src/runtime/agent-memory-state.js +126 -0
  21. package/src/runtime/agent-setup.js +383 -0
  22. package/src/runtime/app-dev-consumers.js +154 -0
  23. package/src/runtime/app-dev-host-lock.js +80 -0
  24. package/src/runtime/app-dev-roots.js +284 -0
  25. package/src/runtime/app-dev-server.js +212 -22
  26. package/src/runtime/app-dev-sessions.js +105 -145
  27. package/src/runtime/app-platform.js +1291 -165
  28. package/src/runtime/app-registry-scaffolds.js +367 -0
  29. package/src/runtime/base-skills.d.ts +20 -0
  30. package/src/runtime/base-skills.js +167 -0
  31. package/src/runtime/login-listener.js +15 -0
  32. package/src/runtime/oauth.js +1506 -141
  33. package/src/runtime/output.js +7 -0
  34. package/src/runtime/ports.js +16 -0
  35. package/src/runtime/profiles.js +0 -5
  36. package/src/runtime/skill-sync/cloud-client.ts +96 -0
  37. package/src/runtime/skill-sync/index.ts +644 -0
  38. package/src/runtime/skill-sync/local-scanner.ts +1046 -0
  39. package/src/runtime/skill-sync/symlink-manager.ts +383 -0
  40. package/src/runtime/skill-sync/sync-plan.ts +22 -0
  41. package/src/runtime/skill-sync/types.ts +103 -0
  42. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  43. package/src/runtime/store-screenshot.js +6 -1
  44. package/src/runtime/sync-skills.d.ts +37 -0
  45. package/src/runtime/sync-skills.js +215 -0
  46. package/template/app/layout.tsx +5 -1
  47. package/template/notis.config.ts +1 -0
  48. package/template/packages/sdk/package.json +2 -1
  49. package/template/packages/sdk/src/components/DocumentEditor.tsx +13 -3
  50. package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
  51. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +37 -119
  52. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
  53. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
  54. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
  55. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  56. package/template/packages/sdk/src/config.ts +19 -0
  57. package/template/packages/sdk/src/documents.ts +7 -1
  58. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  59. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +709 -0
  60. package/template/packages/sdk/src/hooks/useHandover.ts +3 -0
  61. package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
  62. package/template/packages/sdk/src/hooks/useTool.ts +5 -4
  63. package/template/packages/sdk/src/index.ts +47 -2
  64. package/template/packages/sdk/src/interactions/actions.ts +46 -0
  65. package/template/packages/sdk/src/interactions/shortcuts.tsx +628 -0
  66. package/template/packages/sdk/src/interactions.ts +41 -0
  67. package/template/packages/sdk/src/provider.tsx +2 -1
  68. package/template/packages/sdk/src/runtime.ts +89 -4
  69. package/dist/scaffolds/notis-database/CHANGELOG.md +0 -5
  70. package/dist/scaffolds/notis-database/app/globals.css +0 -44
  71. package/dist/scaffolds/notis-database/app/layout.tsx +0 -6
  72. package/dist/scaffolds/notis-database/app/page.tsx +0 -1088
  73. package/dist/scaffolds/notis-database/components/ui/badge.tsx +0 -28
  74. package/dist/scaffolds/notis-database/components/ui/button.tsx +0 -53
  75. package/dist/scaffolds/notis-database/components/ui/card.tsx +0 -56
  76. package/dist/scaffolds/notis-database/components/ui/table.tsx +0 -120
  77. package/dist/scaffolds/notis-database/components.json +0 -20
  78. package/dist/scaffolds/notis-database/index.html +0 -12
  79. package/dist/scaffolds/notis-database/lib/types.ts +0 -132
  80. package/dist/scaffolds/notis-database/lib/utils.ts +0 -6
  81. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  82. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  83. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  84. package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
  85. package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
  86. package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +0 -1839
  87. package/dist/scaffolds/notis-database/notis.config.ts +0 -73
  88. package/dist/scaffolds/notis-database/package-lock.json +0 -3935
  89. package/dist/scaffolds/notis-database/package.json +0 -32
  90. package/dist/scaffolds/notis-database/packages/sdk/package.json +0 -36
  91. package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  92. package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +0 -60
  93. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  94. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  95. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  96. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +0 -234
  97. package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -250
  98. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
  99. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  100. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  101. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  102. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +0 -78
  103. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +0 -121
  104. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useHandover.ts +0 -75
  105. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  106. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +0 -34
  107. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  108. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +0 -64
  109. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +0 -56
  110. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  111. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  112. package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +0 -100
  113. package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
  114. package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -351
  115. package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +0 -186
  116. package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +0 -15
  117. package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +0 -56
  118. package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +0 -15
  119. package/dist/scaffolds/notis-database/postcss.config.mjs +0 -8
  120. package/dist/scaffolds/notis-database/src/dev-main.tsx +0 -23
  121. package/dist/scaffolds/notis-database/src/mock-runtime.ts +0 -561
  122. package/dist/scaffolds/notis-database/tailwind.config.ts +0 -59
  123. package/dist/scaffolds/notis-database/tsconfig.json +0 -23
  124. package/dist/scaffolds/notis-database/vite.config.ts +0 -22
  125. package/dist/scaffolds/notis-journal/CHANGELOG.md +0 -29
  126. package/dist/scaffolds/notis-journal/app/globals.css +0 -37
  127. package/dist/scaffolds/notis-journal/app/insights/page.tsx +0 -513
  128. package/dist/scaffolds/notis-journal/app/journal-core.tsx +0 -362
  129. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +0 -337
  130. package/dist/scaffolds/notis-journal/app/layout.tsx +0 -6
  131. package/dist/scaffolds/notis-journal/app/page.tsx +0 -486
  132. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +0 -28
  133. package/dist/scaffolds/notis-journal/components/ui/button.tsx +0 -53
  134. package/dist/scaffolds/notis-journal/components/ui/card.tsx +0 -56
  135. package/dist/scaffolds/notis-journal/components.json +0 -20
  136. package/dist/scaffolds/notis-journal/index.html +0 -12
  137. package/dist/scaffolds/notis-journal/lib/utils.ts +0 -6
  138. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  139. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  140. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  141. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  142. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  143. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  144. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +0 -132
  145. package/dist/scaffolds/notis-journal/notis.config.ts +0 -93
  146. package/dist/scaffolds/notis-journal/package-lock.json +0 -4615
  147. package/dist/scaffolds/notis-journal/package.json +0 -34
  148. package/dist/scaffolds/notis-journal/packages/sdk/package.json +0 -36
  149. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  150. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +0 -60
  151. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  152. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  153. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  154. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +0 -234
  155. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -250
  156. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
  157. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  158. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  159. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  160. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +0 -78
  161. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +0 -121
  162. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useHandover.ts +0 -75
  163. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  164. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +0 -34
  165. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  166. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +0 -64
  167. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +0 -56
  168. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  169. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  170. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +0 -100
  171. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
  172. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -351
  173. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +0 -186
  174. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +0 -15
  175. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +0 -56
  176. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +0 -15
  177. package/dist/scaffolds/notis-journal/postcss.config.mjs +0 -8
  178. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +0 -120
  179. package/dist/scaffolds/notis-journal/src/dev-main.tsx +0 -58
  180. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +0 -199
  181. package/dist/scaffolds/notis-journal/tailwind.config.ts +0 -58
  182. package/dist/scaffolds/notis-journal/tsconfig.json +0 -23
  183. package/dist/scaffolds/notis-journal/vite.config.ts +0 -10
  184. package/dist/scaffolds/notis-notes/CHANGELOG.md +0 -5
  185. package/dist/scaffolds/notis-notes/app/globals.css +0 -3
  186. package/dist/scaffolds/notis-notes/app/layout.tsx +0 -6
  187. package/dist/scaffolds/notis-notes/app/page.tsx +0 -1943
  188. package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +0 -596
  189. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +0 -28
  190. package/dist/scaffolds/notis-notes/components/ui/button.tsx +0 -53
  191. package/dist/scaffolds/notis-notes/components/ui/card.tsx +0 -56
  192. package/dist/scaffolds/notis-notes/components.json +0 -20
  193. package/dist/scaffolds/notis-notes/lib/utils.ts +0 -6
  194. package/dist/scaffolds/notis-notes/lib/visible-properties.ts +0 -144
  195. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  196. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  197. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  198. package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
  199. package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
  200. package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +0 -752
  201. package/dist/scaffolds/notis-notes/notis.config.ts +0 -80
  202. package/dist/scaffolds/notis-notes/package-lock.json +0 -4636
  203. package/dist/scaffolds/notis-notes/package.json +0 -35
  204. package/dist/scaffolds/notis-notes/packages/sdk/package.json +0 -36
  205. package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  206. package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +0 -60
  207. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  208. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  209. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  210. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +0 -234
  211. package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -250
  212. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
  213. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  214. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  215. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  216. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +0 -78
  217. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +0 -121
  218. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useHandover.ts +0 -75
  219. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  220. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +0 -34
  221. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  222. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +0 -64
  223. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +0 -56
  224. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  225. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  226. package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +0 -100
  227. package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
  228. package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -351
  229. package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +0 -186
  230. package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +0 -15
  231. package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +0 -56
  232. package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +0 -15
  233. package/dist/scaffolds/notis-notes/postcss.config.mjs +0 -8
  234. package/dist/scaffolds/notis-notes/tailwind.config.ts +0 -58
  235. package/dist/scaffolds/notis-notes/tsconfig.json +0 -23
  236. package/dist/scaffolds/notis-notes/vite.config.ts +0 -10
  237. package/dist/scaffolds/notis-random/CHANGELOG.md +0 -15
  238. package/dist/scaffolds/notis-random/README.md +0 -33
  239. package/dist/scaffolds/notis-random/app/globals.css +0 -11
  240. package/dist/scaffolds/notis-random/app/history/page.tsx +0 -67
  241. package/dist/scaffolds/notis-random/app/layout.tsx +0 -7
  242. package/dist/scaffolds/notis-random/app/page.tsx +0 -289
  243. package/dist/scaffolds/notis-random/components/ui/button.tsx +0 -50
  244. package/dist/scaffolds/notis-random/components/ui/card.tsx +0 -16
  245. package/dist/scaffolds/notis-random/components/ui/input.tsx +0 -23
  246. package/dist/scaffolds/notis-random/components.json +0 -20
  247. package/dist/scaffolds/notis-random/index.html +0 -12
  248. package/dist/scaffolds/notis-random/lib/notis-tools.ts +0 -128
  249. package/dist/scaffolds/notis-random/lib/rng.ts +0 -202
  250. package/dist/scaffolds/notis-random/lib/roll-record.ts +0 -189
  251. package/dist/scaffolds/notis-random/lib/utils.ts +0 -25
  252. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  253. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  254. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  255. package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
  256. package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
  257. package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +0 -753
  258. package/dist/scaffolds/notis-random/notis.config.ts +0 -86
  259. package/dist/scaffolds/notis-random/package-lock.json +0 -4513
  260. package/dist/scaffolds/notis-random/package.json +0 -36
  261. package/dist/scaffolds/notis-random/packages/sdk/package.json +0 -36
  262. package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  263. package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +0 -60
  264. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  265. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  266. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  267. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +0 -234
  268. package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -250
  269. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
  270. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  271. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  272. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  273. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +0 -78
  274. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +0 -121
  275. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useHandover.ts +0 -75
  276. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  277. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +0 -34
  278. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  279. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +0 -64
  280. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +0 -56
  281. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  282. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  283. package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +0 -100
  284. package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
  285. package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -351
  286. package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +0 -186
  287. package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +0 -15
  288. package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +0 -56
  289. package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +0 -15
  290. package/dist/scaffolds/notis-random/postcss.config.mjs +0 -6
  291. package/dist/scaffolds/notis-random/src/dev-main.tsx +0 -70
  292. package/dist/scaffolds/notis-random/src/mock-runtime.ts +0 -129
  293. package/dist/scaffolds/notis-random/tailwind.config.ts +0 -50
  294. package/dist/scaffolds/notis-random/tsconfig.json +0 -23
  295. package/dist/scaffolds/notis-random/vite.config.ts +0 -11
  296. package/dist/scaffolds.json +0 -49
@@ -0,0 +1,644 @@
1
+ import type {
2
+ LocalSkill,
3
+ NotisSyncState,
4
+ SkillSyncFailure,
5
+ SyncPullResponse,
6
+ SyncSettings,
7
+ } from "./types";
8
+ import { normalizeAgentTargets } from "./types";
9
+ import {
10
+ downloadSkillBundle,
11
+ fetchSyncSettings,
12
+ pullSkills,
13
+ pushChangedSkills,
14
+ updateAgentTargets,
15
+ } from "./cloud-client";
16
+ import {
17
+ deleteLocalSkill,
18
+ gatherTopLevelLocalSkills,
19
+ getSkillSyncPathsForUser,
20
+ readLegacySyncState,
21
+ readSyncState,
22
+ scanLocalSkills,
23
+ writeCloudSkillToDisk,
24
+ writeSyncState,
25
+ } from "./local-scanner";
26
+ import {
27
+ detectDeletedAgentSymlinks,
28
+ removeForeignAccountSymlinks,
29
+ removeAllSymlinksForSkill,
30
+ syncSymlinks,
31
+ type DeletedAgentSymlink,
32
+ } from "./symlink-manager";
33
+ import { getPushCandidates } from "./sync-plan";
34
+ import { writeCloudSkillWithBundleFallback } from "./write-cloud-skill";
35
+
36
+ export interface RunSkillSyncResult {
37
+ syncEnabled: boolean;
38
+ pushed: number;
39
+ pulled: number;
40
+ downloaded: number;
41
+ deleted: number;
42
+ /** Skills deactivated for a specific agent because the user deleted that agent's local
43
+ * symlink (e.g. `rm ~/.claude/skills/<skill>`); the deletion is honored instead of recreated. */
44
+ deactivated: number;
45
+ linked: number;
46
+ removed: number;
47
+ skipped: number;
48
+ lastSyncedAt: string | null;
49
+ /** Skills the server rejected (e.g. an invalid SKILL.md description). The rest of
50
+ * the batch still syncs; these are surfaced so the user knows what to fix. */
51
+ failedPushes: SkillSyncFailure[];
52
+ }
53
+
54
+ export interface RunSkillSyncOptions {
55
+ /** Electron's scheduled invocation honors the account preference. A manual
56
+ * CLI sync always runs, even when automatic Desktop refresh is disabled. */
57
+ honorSyncEnabled?: boolean;
58
+ }
59
+
60
+ const BASE_SKILL_NAMES = new Set(['notis-apps', 'notis-query', 'notis-cli']);
61
+
62
+ function withoutBaseSkills(pullResponse: SyncPullResponse): SyncPullResponse {
63
+ return {
64
+ ...pullResponse,
65
+ skills: pullResponse.skills.filter((skill) => !BASE_SKILL_NAMES.has(skill.name)),
66
+ };
67
+ }
68
+
69
+ function withoutBaseSkillState(state: NotisSyncState): NotisSyncState {
70
+ return {
71
+ ...state,
72
+ skills: Object.fromEntries(
73
+ Object.entries(state.skills).filter(([name]) => !BASE_SKILL_NAMES.has(name)),
74
+ ),
75
+ };
76
+ }
77
+
78
+ export interface MaterializeCloudSkillsResult {
79
+ pulled: number;
80
+ downloaded: number;
81
+ deleted: number;
82
+ removed: number;
83
+ lastSyncedAt: string | null;
84
+ }
85
+
86
+ export interface MaterializeCloudSkillsOptions {
87
+ /** Re-link only these cloud skills without removing or adopting other links. */
88
+ relinkSkillNames?: readonly string[];
89
+ }
90
+
91
+ interface RunSkillSyncDependencies {
92
+ fetchSyncSettings: typeof fetchSyncSettings;
93
+ pullSkills: typeof pullSkills;
94
+ pushChangedSkills: typeof pushChangedSkills;
95
+ downloadSkillBundle: typeof downloadSkillBundle;
96
+ gatherTopLevelLocalSkills: typeof gatherTopLevelLocalSkills;
97
+ readLegacySyncState: typeof readLegacySyncState;
98
+ readSyncState: typeof readSyncState;
99
+ scanLocalSkills: typeof scanLocalSkills;
100
+ deleteLocalSkill: typeof deleteLocalSkill;
101
+ writeCloudSkillToDisk: typeof writeCloudSkillToDisk;
102
+ writeSyncState: typeof writeSyncState;
103
+ removeAllSymlinksForSkill: typeof removeAllSymlinksForSkill;
104
+ syncSymlinks: typeof syncSymlinks;
105
+ detectDeletedAgentSymlinks: typeof detectDeletedAgentSymlinks;
106
+ removeForeignAccountSymlinks: typeof removeForeignAccountSymlinks;
107
+ updateAgentTargets: typeof updateAgentTargets;
108
+ }
109
+
110
+ const DEFAULT_RUN_SKILL_SYNC_DEPS: RunSkillSyncDependencies = {
111
+ fetchSyncSettings,
112
+ pullSkills,
113
+ pushChangedSkills,
114
+ downloadSkillBundle,
115
+ gatherTopLevelLocalSkills,
116
+ readLegacySyncState,
117
+ readSyncState,
118
+ scanLocalSkills,
119
+ deleteLocalSkill,
120
+ writeCloudSkillToDisk,
121
+ writeSyncState,
122
+ removeAllSymlinksForSkill,
123
+ syncSymlinks,
124
+ detectDeletedAgentSymlinks,
125
+ removeForeignAccountSymlinks,
126
+ updateAgentTargets,
127
+ };
128
+ function toSkillMap(skills: LocalSkill[]): Map<string, LocalSkill> {
129
+ return new Map(skills.map((skill) => [skill.name, skill]));
130
+ }
131
+
132
+ function decodeJwtSubject(jwt: string): string | null {
133
+ try {
134
+ const parts = jwt.split(".");
135
+ if (parts.length !== 3) return null;
136
+ const decoded = JSON.parse(
137
+ Buffer.from(parts[1], "base64url").toString(),
138
+ ) as { sub?: unknown };
139
+ return typeof decoded.sub === "string" && decoded.sub.trim()
140
+ ? decoded.sub.trim()
141
+ : null;
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ function shouldWriteCloudSkill(
148
+ cloudSkill: SyncPullResponse["skills"][number],
149
+ localSkills: Map<string, LocalSkill>,
150
+ previousState: NotisSyncState,
151
+ ): boolean {
152
+ const skillName = cloudSkill.name;
153
+ const cloudHash = cloudSkill.skill_folder_hash || "";
154
+ const localSkill = localSkills.get(skillName);
155
+ if (!localSkill) {
156
+ return true;
157
+ }
158
+
159
+ if (cloudSkill.source === "curated") {
160
+ return cloudHash ? cloudHash !== localSkill.folderHash : true;
161
+ }
162
+
163
+ const previous = previousState.skills[skillName];
164
+ const localChangedSinceLastSync =
165
+ !previous || previous.folderHash !== localSkill.folderHash;
166
+ return (
167
+ !localChangedSinceLastSync &&
168
+ Boolean(cloudHash) &&
169
+ cloudHash !== localSkill.folderHash
170
+ );
171
+ }
172
+
173
+ function buildSyncState(
174
+ pullResponse: SyncPullResponse,
175
+ localSkills: LocalSkill[],
176
+ lastSyncedAt: string | null,
177
+ ): NotisSyncState {
178
+ const localSkillMap = toSkillMap(localSkills);
179
+ const skills = Object.fromEntries(
180
+ pullResponse.skills.map((skill) => {
181
+ const localSkill = localSkillMap.get(skill.name);
182
+ return [
183
+ skill.name,
184
+ {
185
+ cloudId: skill.id,
186
+ folderHash: localSkill?.folderHash || skill.skill_folder_hash || "",
187
+ agentTargets: normalizeAgentTargets(skill.agent_targets),
188
+ syncedAt: lastSyncedAt || new Date().toISOString(),
189
+ },
190
+ ];
191
+ }),
192
+ );
193
+
194
+ return {
195
+ version: 1,
196
+ lastSyncedAt,
197
+ skills,
198
+ };
199
+ }
200
+
201
+ function buildLocalSymlinkCandidates(
202
+ pullResponse: SyncPullResponse,
203
+ localSkills: LocalSkill[],
204
+ previousState: NotisSyncState,
205
+ ): SyncPullResponse["skills"] {
206
+ const cloudSkillNames = new Set(
207
+ pullResponse.skills.map((skill) => skill.name),
208
+ );
209
+ const localOnlySkills = localSkills
210
+ .filter((skill) => !cloudSkillNames.has(skill.name))
211
+ .map((skill) => {
212
+ const previous = previousState.skills[skill.name];
213
+ return {
214
+ id: previous?.cloudId || `local-${skill.name}`,
215
+ name: skill.name,
216
+ description: skill.description || null,
217
+ skill_md: skill.skillMd,
218
+ agent_targets: previous?.agentTargets,
219
+ skill_folder_hash: skill.folderHash,
220
+ source: "local",
221
+ status: "active",
222
+ };
223
+ });
224
+
225
+ return [...pullResponse.skills, ...localOnlySkills];
226
+ }
227
+
228
+ function isEmptySyncState(state: NotisSyncState): boolean {
229
+ return state.lastSyncedAt === null && Object.keys(state.skills).length === 0;
230
+ }
231
+
232
+ function applyLegacyFirstRunState(
233
+ localSkills: LocalSkill[],
234
+ scopedState: NotisSyncState,
235
+ legacyState: NotisSyncState | null,
236
+ ): NotisSyncState {
237
+ if (!isEmptySyncState(scopedState) || !legacyState) {
238
+ return scopedState;
239
+ }
240
+
241
+ const migratedSkills = Object.fromEntries(
242
+ localSkills.flatMap((skill) => {
243
+ const previous = legacyState.skills[skill.name];
244
+ if (!previous || previous.folderHash !== skill.folderHash) {
245
+ return [];
246
+ }
247
+ return [[skill.name, previous]];
248
+ }),
249
+ );
250
+
251
+ if (Object.keys(migratedSkills).length === 0) {
252
+ return scopedState;
253
+ }
254
+
255
+ return {
256
+ version: 1,
257
+ lastSyncedAt: legacyState.lastSyncedAt,
258
+ skills: migratedSkills,
259
+ };
260
+ }
261
+
262
+ async function writePulledSkillsToScopedMirror(
263
+ pullResponse: SyncPullResponse,
264
+ localSkills: LocalSkill[],
265
+ previousState: NotisSyncState,
266
+ syncPaths: ReturnType<typeof getSkillSyncPathsForUser>,
267
+ deps: Pick<
268
+ RunSkillSyncDependencies,
269
+ "downloadSkillBundle" | "writeCloudSkillToDisk"
270
+ >,
271
+ ): Promise<number> {
272
+ const localSkillMap = toSkillMap(localSkills);
273
+ const warnSkillSync = (message: string, error: unknown): void => {
274
+ console.warn(`[Notis] ${message}`, error);
275
+ };
276
+
277
+ let downloaded = 0;
278
+ for (const cloudSkill of pullResponse.skills) {
279
+ if (!shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
280
+ continue;
281
+ }
282
+
283
+ if (
284
+ await writeCloudSkillWithBundleFallback(cloudSkill, {
285
+ downloadSkillBundle: deps.downloadSkillBundle,
286
+ writeCloudSkillToDisk: (skill, bundleBytes) =>
287
+ deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths),
288
+ onWarning: warnSkillSync,
289
+ })
290
+ ) {
291
+ downloaded += 1;
292
+ }
293
+ }
294
+
295
+ return downloaded;
296
+ }
297
+
298
+ function assertSkillsPullAuthorized(
299
+ pullResponse: SyncPullResponse,
300
+ ): void {
301
+ if (
302
+ pullResponse.entitlement_access?.code === "entitlement_upgrade_required"
303
+ && pullResponse.entitlement_access.entitlement === "skills"
304
+ ) {
305
+ // Current servers return HTTP 403, so cloud-client rejects before producing
306
+ // a SyncPullResponse. Keep this guard for an older server's successful
307
+ // empty-denial envelope, but fail closed: it is never authorization to
308
+ // delete a user's managed local mirror.
309
+ throw new Error(
310
+ "Skill sync access was denied; preserving existing local skills.",
311
+ );
312
+ }
313
+ }
314
+
315
+ export async function materializeCloudSkillsForLocalShell(
316
+ serverUrl: string,
317
+ jwt: string,
318
+ dependencies: Partial<Pick<
319
+ RunSkillSyncDependencies,
320
+ | "downloadSkillBundle"
321
+ | "deleteLocalSkill"
322
+ | "pullSkills"
323
+ | "readSyncState"
324
+ | "removeAllSymlinksForSkill"
325
+ | "scanLocalSkills"
326
+ | "syncSymlinks"
327
+ | "writeCloudSkillToDisk"
328
+ | "writeSyncState"
329
+ >> = {},
330
+ options: MaterializeCloudSkillsOptions = {},
331
+ ): Promise<MaterializeCloudSkillsResult> {
332
+ const deps = {
333
+ ...DEFAULT_RUN_SKILL_SYNC_DEPS,
334
+ ...dependencies,
335
+ };
336
+ const authUserId = decodeJwtSubject(jwt);
337
+ if (!authUserId) {
338
+ throw new Error(
339
+ "Cannot materialize skills without a valid authenticated desktop session.",
340
+ );
341
+ }
342
+
343
+ const syncPaths = getSkillSyncPathsForUser(authUserId);
344
+ const pullResponse = await deps.pullSkills(serverUrl, jwt);
345
+ assertSkillsPullAuthorized(pullResponse);
346
+ const previousState = await deps.readSyncState(syncPaths);
347
+
348
+ const localSkills = await deps.scanLocalSkills(syncPaths);
349
+ const downloaded = await writePulledSkillsToScopedMirror(
350
+ pullResponse,
351
+ localSkills,
352
+ previousState,
353
+ syncPaths,
354
+ deps,
355
+ );
356
+ const finalLocalSkills = await deps.scanLocalSkills(syncPaths);
357
+ const lastSyncedAt = pullResponse.last_synced_at || new Date().toISOString();
358
+
359
+ const relinkSkillNames = new Set(options.relinkSkillNames || []);
360
+ if (relinkSkillNames.size > 0) {
361
+ await deps.syncSymlinks(
362
+ pullResponse.skills.filter((skill) => relinkSkillNames.has(skill.name)),
363
+ syncPaths.skillsDir,
364
+ { removeUndesired: false },
365
+ );
366
+ }
367
+
368
+ await deps.writeSyncState(
369
+ buildSyncState(pullResponse, finalLocalSkills, lastSyncedAt),
370
+ syncPaths,
371
+ );
372
+
373
+ return {
374
+ pulled: pullResponse.skills.length,
375
+ downloaded,
376
+ deleted: 0,
377
+ removed: 0,
378
+ lastSyncedAt,
379
+ };
380
+ }
381
+
382
+ /**
383
+ * When the user deletes a skill's symlink for a single agent (e.g. `rm ~/.claude/skills/<skill>`),
384
+ * honor that as "remove this skill from that agent" by deactivating it in the portal, instead of
385
+ * recreating the symlink on the next sync. The cloud `agent_targets` are mutated in place so the
386
+ * subsequent symlink reconciliation treats the link as undesired. Returns the number of
387
+ * (skill, agent) pairs deactivated.
388
+ */
389
+ async function deactivateDeletedAgentSkills(
390
+ serverUrl: string,
391
+ jwt: string,
392
+ pullResponse: SyncPullResponse,
393
+ previousState: NotisSyncState,
394
+ scopedState: NotisSyncState,
395
+ skillsDir: string,
396
+ deps: Pick<
397
+ RunSkillSyncDependencies,
398
+ "detectDeletedAgentSymlinks" | "updateAgentTargets" | "pullSkills"
399
+ >,
400
+ ): Promise<number> {
401
+ // First sync (incl. legacy migration) has no reliable "we created this link" signal, so we
402
+ // cannot tell a user deletion apart from a never-created link — skip detection entirely.
403
+ if (isEmptySyncState(scopedState)) {
404
+ return 0;
405
+ }
406
+
407
+ const deletions = await deps.detectDeletedAgentSymlinks(
408
+ pullResponse.skills,
409
+ previousState,
410
+ skillsDir,
411
+ );
412
+ if (deletions.length === 0) {
413
+ return 0;
414
+ }
415
+
416
+ // The agent-targets endpoint replaces the whole agent_targets column, so re-read the freshest
417
+ // values right before writing and merge onto them, only flipping the deleted agent. This avoids
418
+ // clobbering a concurrent portal/other-desktop change to a DIFFERENT agent with the stale
419
+ // snapshot from the top-of-sync pull. (A narrow read-modify-write window remains; the endpoint
420
+ // has no partial update.)
421
+ const latestSkillsById = new Map(pullResponse.skills.map((skill) => [skill.id, skill]));
422
+ let fresh: SyncPullResponse | null = null;
423
+ try {
424
+ fresh = await deps.pullSkills(serverUrl, jwt);
425
+ } catch (error) {
426
+ console.warn(
427
+ "[skill-sync] Could not re-pull latest agent targets before deactivation; " +
428
+ "using the top-of-sync snapshot.",
429
+ error,
430
+ );
431
+ }
432
+ if (fresh) {
433
+ // A transport failure may fall back to the already-authorized snapshot, but
434
+ // an explicit legacy denial must abort before any server or local mutation.
435
+ assertSkillsPullAuthorized(fresh);
436
+ for (const skill of fresh.skills) {
437
+ latestSkillsById.set(skill.id, skill);
438
+ }
439
+ }
440
+
441
+ // Group deletions by skill so multiple deleted agents for the SAME skill are flipped in one
442
+ // PATCH. Otherwise each PATCH, rebuilt from the same snapshot, would overwrite the previous one
443
+ // (deleting both claude_code and cursor for one skill would otherwise leave only the last off).
444
+ const agentsBySkill = new Map<string, { skillName: string; agents: Set<DeletedAgentSymlink["agent"]> }>();
445
+ for (const deletion of deletions) {
446
+ const entry = agentsBySkill.get(deletion.skillId) ?? {
447
+ skillName: deletion.skillName,
448
+ agents: new Set<DeletedAgentSymlink["agent"]>(),
449
+ };
450
+ entry.agents.add(deletion.agent);
451
+ agentsBySkill.set(deletion.skillId, entry);
452
+ }
453
+
454
+ const inMemoryById = new Map(pullResponse.skills.map((skill) => [skill.id, skill]));
455
+ let deactivated = 0;
456
+ for (const [skillId, { skillName, agents }] of agentsBySkill) {
457
+ const latest = latestSkillsById.get(skillId);
458
+ if (!latest) {
459
+ continue;
460
+ }
461
+ const nextTargets = { ...normalizeAgentTargets(latest.agent_targets) };
462
+ for (const agent of agents) {
463
+ nextTargets[agent] = false;
464
+ }
465
+ try {
466
+ await deps.updateAgentTargets(serverUrl, jwt, skillId, nextTargets);
467
+ // Mutate the in-memory pull response so the symlink passes below see the link as undesired.
468
+ const inMemory = inMemoryById.get(skillId);
469
+ if (inMemory) {
470
+ inMemory.agent_targets = nextTargets;
471
+ }
472
+ deactivated += agents.size;
473
+ } catch (error) {
474
+ console.warn(
475
+ `[skill-sync] Failed to deactivate "${skillName}" for ${[...agents].join(", ")} ` +
476
+ `after local symlink deletion:`,
477
+ error,
478
+ );
479
+ }
480
+ }
481
+ return deactivated;
482
+ }
483
+
484
+ export async function runSkillSync(
485
+ serverUrl: string,
486
+ jwt: string,
487
+ dependencies: Partial<RunSkillSyncDependencies> = {},
488
+ options: RunSkillSyncOptions = {},
489
+ ): Promise<RunSkillSyncResult> {
490
+ const deps = {
491
+ ...DEFAULT_RUN_SKILL_SYNC_DEPS,
492
+ ...dependencies,
493
+ };
494
+ const syncSettings: SyncSettings = await deps.fetchSyncSettings(
495
+ serverUrl,
496
+ jwt,
497
+ );
498
+ // Supabase Desktop sessions use the auth-user id as JWT `sub`, while CLI
499
+ // OAuth credentials use the canonical Notis `users.user_id`. Trust the
500
+ // authenticated server response so both transports share one local mirror.
501
+ // The token fallback keeps the CLI compatible with an older server during
502
+ // rollout, where sync-settings did not yet return user_id.
503
+ const syncUserId = syncSettings.user_id?.trim() || decodeJwtSubject(jwt);
504
+ if (!syncUserId) {
505
+ throw new Error(
506
+ "Cannot sync skills without a server-verified account identity.",
507
+ );
508
+ }
509
+ const syncPaths = getSkillSyncPathsForUser(syncUserId);
510
+ const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
511
+ syncPaths.skillsDir,
512
+ );
513
+ if (options.honorSyncEnabled !== false && !syncSettings.sync_enabled) {
514
+ return {
515
+ syncEnabled: false,
516
+ pushed: 0,
517
+ pulled: 0,
518
+ downloaded: 0,
519
+ deleted: 0,
520
+ deactivated: 0,
521
+ linked: 0,
522
+ removed: foreignLinksRemoved,
523
+ skipped: 0,
524
+ lastSyncedAt: syncSettings.last_synced_at,
525
+ failedPushes: [],
526
+ };
527
+ }
528
+
529
+ let pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
530
+ assertSkillsPullAuthorized(pullResponse);
531
+
532
+ const cloudCuratedSkillNames = new Set(
533
+ pullResponse.skills
534
+ .filter((skill) => skill.source === "curated")
535
+ .map((skill) => skill.name),
536
+ );
537
+ const protectedSkillNames = new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
538
+ const authUserId = decodeJwtSubject(jwt);
539
+ let previousAuthState: NotisSyncState | null = null;
540
+ if (authUserId && authUserId !== syncUserId) {
541
+ const previousAuthPaths = getSkillSyncPathsForUser(authUserId);
542
+ previousAuthState = await deps.readSyncState(previousAuthPaths);
543
+ await deps.gatherTopLevelLocalSkills(syncPaths, {
544
+ sourceRoots: [{ label: "previous-auth-scope", root: previousAuthPaths.skillsDir }],
545
+ protectedSkillNames,
546
+ });
547
+ }
548
+ await deps.gatherTopLevelLocalSkills(syncPaths, {
549
+ protectedSkillNames,
550
+ });
551
+ const localSkills = (await deps.scanLocalSkills(syncPaths))
552
+ .filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
553
+ const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
554
+ const previousState = withoutBaseSkillState(applyLegacyFirstRunState(
555
+ localSkills,
556
+ scopedState,
557
+ isEmptySyncState(scopedState)
558
+ ? (!previousAuthState || isEmptySyncState(previousAuthState)
559
+ ? await deps.readLegacySyncState(syncPaths)
560
+ : previousAuthState)
561
+ : null,
562
+ ));
563
+ // Honor locally-deleted per-agent symlinks as deactivations before reconciling, otherwise the
564
+ // first syncSymlinks pass would recreate the link the user just deleted.
565
+ const deactivated = await deactivateDeletedAgentSkills(
566
+ serverUrl,
567
+ jwt,
568
+ pullResponse,
569
+ previousState,
570
+ scopedState,
571
+ syncPaths.skillsDir,
572
+ deps,
573
+ );
574
+ await deps.syncSymlinks(
575
+ buildLocalSymlinkCandidates(pullResponse, localSkills, previousState),
576
+ syncPaths.skillsDir,
577
+ );
578
+ const pushCandidates = getPushCandidates(
579
+ localSkills,
580
+ previousState,
581
+ cloudCuratedSkillNames,
582
+ new Set(pullResponse.skills.map((skill) => skill.name)),
583
+ );
584
+
585
+ const failedPushes: SkillSyncFailure[] = [];
586
+ if (pushCandidates.length > 0) {
587
+ const pushResult = await deps.pushChangedSkills(serverUrl, jwt, pushCandidates);
588
+ if (Array.isArray(pushResult?.failed) && pushResult.failed.length > 0) {
589
+ failedPushes.push(...pushResult.failed);
590
+ console.warn(
591
+ `[skill-sync] ${pushResult.failed.length} skill(s) were rejected during push: ` +
592
+ pushResult.failed.map((f) => `${f.name} (${f.error})`).join("; "),
593
+ );
594
+ }
595
+ pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
596
+ assertSkillsPullAuthorized(pullResponse);
597
+ }
598
+
599
+ // Delete phase: remove skills that were previously synced but are no longer in the cloud
600
+ const cloudSkillNames = new Set(pullResponse.skills.map((s) => s.name));
601
+ let deleted = 0;
602
+ for (const skillName of Object.keys(previousState.skills)) {
603
+ if (!cloudSkillNames.has(skillName)) {
604
+ await deps.deleteLocalSkill(skillName, syncPaths);
605
+ await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir);
606
+ deleted += 1;
607
+ }
608
+ }
609
+
610
+ const downloaded = await writePulledSkillsToScopedMirror(
611
+ pullResponse,
612
+ localSkills,
613
+ previousState,
614
+ syncPaths,
615
+ deps,
616
+ );
617
+
618
+ const finalLocalSkills = (await deps.scanLocalSkills(syncPaths))
619
+ .filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
620
+ const symlinkResult = await deps.syncSymlinks(
621
+ buildLocalSymlinkCandidates(pullResponse, finalLocalSkills, previousState),
622
+ syncPaths.skillsDir,
623
+ );
624
+ const lastSyncedAt = pullResponse.last_synced_at || new Date().toISOString();
625
+
626
+ await deps.writeSyncState(
627
+ buildSyncState(pullResponse, finalLocalSkills, lastSyncedAt),
628
+ syncPaths,
629
+ );
630
+
631
+ return {
632
+ syncEnabled: true,
633
+ pushed: pushCandidates.length,
634
+ pulled: pullResponse.skills.length,
635
+ downloaded,
636
+ deleted,
637
+ deactivated,
638
+ linked: symlinkResult.linked,
639
+ removed: foreignLinksRemoved + symlinkResult.removed,
640
+ skipped: symlinkResult.skipped,
641
+ lastSyncedAt,
642
+ failedPushes,
643
+ };
644
+ }