@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,1046 @@
1
+ import { createHash } from "crypto";
2
+ import { execFile } from "child_process";
3
+ import type { Dirent } from "fs";
4
+ import { promises as fs } from "fs";
5
+ import os from "os";
6
+ import path from "path";
7
+ import { promisify } from "util";
8
+
9
+ import type { CloudSkill, LocalSkill, NotisSyncState } from "./types";
10
+
11
+ const HOME_DIR = os.homedir();
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ export const AGENTS_DIR = path.join(HOME_DIR, ".agents");
15
+ export const LEGACY_AGENTS_SKILLS_DIR = path.join(AGENTS_DIR, "skills");
16
+ export const NOTIS_SKILL_SYNC_ROOT = path.join(HOME_DIR, ".notis", "skills");
17
+ export const LEGACY_NOTIS_SYNC_STATE_PATH = path.join(
18
+ AGENTS_DIR,
19
+ ".notis-sync.json",
20
+ );
21
+ export const AGENTS_SKILLS_DIR = LEGACY_AGENTS_SKILLS_DIR;
22
+ export const NOTIS_SYNC_STATE_PATH = LEGACY_NOTIS_SYNC_STATE_PATH;
23
+ const SKILL_LOCK_PATH = path.join(AGENTS_DIR, ".skill-lock.json");
24
+
25
+ const DEFAULT_SYNC_STATE: NotisSyncState = {
26
+ version: 1,
27
+ lastSyncedAt: null,
28
+ skills: {},
29
+ };
30
+ const EXCLUDED_TOP_LEVEL_ROOT_NAMES = new Set([
31
+ "backup",
32
+ "backups",
33
+ "builtin",
34
+ "builtins",
35
+ "cache",
36
+ "caches",
37
+ "marketplace",
38
+ "marketplaces",
39
+ "plugin",
40
+ "plugins",
41
+ "temp",
42
+ "tmp",
43
+ "worktree",
44
+ "worktrees",
45
+ ]);
46
+
47
+ export interface SkillSyncPaths {
48
+ agentsDir: string;
49
+ syncRoot: string;
50
+ legacySkillsDir: string;
51
+ legacyScopedSkillsDir: string;
52
+ legacyScopedSyncStatePath: string;
53
+ skillsDir: string;
54
+ syncStatePath: string;
55
+ skillLockPath: string;
56
+ gatherMetadataPath: string;
57
+ }
58
+
59
+ const DEFAULT_SYNC_PATHS: SkillSyncPaths = {
60
+ agentsDir: AGENTS_DIR,
61
+ syncRoot: NOTIS_SKILL_SYNC_ROOT,
62
+ legacySkillsDir: LEGACY_AGENTS_SKILLS_DIR,
63
+ legacyScopedSkillsDir: LEGACY_AGENTS_SKILLS_DIR,
64
+ legacyScopedSyncStatePath: LEGACY_NOTIS_SYNC_STATE_PATH,
65
+ skillsDir: LEGACY_AGENTS_SKILLS_DIR,
66
+ syncStatePath: LEGACY_NOTIS_SYNC_STATE_PATH,
67
+ skillLockPath: SKILL_LOCK_PATH,
68
+ gatherMetadataPath: path.join(NOTIS_SKILL_SYNC_ROOT, "skill-gather-metadata.json"),
69
+ };
70
+
71
+ function getDefaultSyncRootForAgentsDir(resolvedAgentsDir: string): string {
72
+ if (resolvedAgentsDir === path.resolve(AGENTS_DIR)) {
73
+ return NOTIS_SKILL_SYNC_ROOT;
74
+ }
75
+ return path.join(resolvedAgentsDir, ".notis", "skills");
76
+ }
77
+
78
+ function isResolvedChildPath(baseDir: string, candidatePath: string): boolean {
79
+ return candidatePath.startsWith(`${baseDir}${path.sep}`);
80
+ }
81
+
82
+ function sanitizePathSegment(value: string): string {
83
+ const raw = value.trim();
84
+ const sanitized = raw
85
+ .replace(/[^A-Za-z0-9_-]+/g, "_")
86
+ .replace(/^_+|_+$/g, "");
87
+ if (!sanitized) {
88
+ throw new Error("Invalid sync user id");
89
+ }
90
+ if (sanitized === raw) {
91
+ return sanitized;
92
+ }
93
+ return `${sanitized}-${createHash("sha256").update(raw).digest("hex").slice(0, 12)}`;
94
+ }
95
+
96
+ export function getSkillSyncPathsForUser(
97
+ authUserId: string,
98
+ agentsDir: string = AGENTS_DIR,
99
+ ): SkillSyncPaths {
100
+ const safeUserId = sanitizePathSegment(authUserId);
101
+ const resolvedAgentsDir = path.resolve(agentsDir);
102
+ const syncRoot = getDefaultSyncRootForAgentsDir(resolvedAgentsDir);
103
+ const userRoot = path.join(syncRoot, "users", safeUserId);
104
+ const legacyUserRoot = path.join(resolvedAgentsDir, "notis", "users", safeUserId);
105
+
106
+ return {
107
+ agentsDir: resolvedAgentsDir,
108
+ syncRoot,
109
+ legacySkillsDir: path.join(resolvedAgentsDir, "skills"),
110
+ legacyScopedSkillsDir: path.join(legacyUserRoot, "skills"),
111
+ legacyScopedSyncStatePath: path.join(legacyUserRoot, ".notis-sync.json"),
112
+ skillsDir: path.join(userRoot, "skills"),
113
+ syncStatePath: path.join(userRoot, ".notis-sync.json"),
114
+ skillLockPath: path.join(resolvedAgentsDir, ".skill-lock.json"),
115
+ gatherMetadataPath: path.join(userRoot, ".notis-gathered-skills.json"),
116
+ };
117
+ }
118
+
119
+ function assertRelativeBundlePath(relativePath: string): string {
120
+ const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
121
+ if (
122
+ !normalized ||
123
+ normalized.startsWith("../") ||
124
+ `/${normalized}/`.includes("/../")
125
+ ) {
126
+ throw new Error(`Invalid bundle file path: ${relativePath}`);
127
+ }
128
+ return normalized;
129
+ }
130
+
131
+ function stripWrappingQuotes(value: string): string {
132
+ return value.replace(/^['"]|['"]$/g, "").trim();
133
+ }
134
+
135
+ function parseFrontMatter(skillMd: string): { description: string } {
136
+ const match = skillMd.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
137
+ if (!match) {
138
+ return { description: "" };
139
+ }
140
+
141
+ let description = "";
142
+ for (const line of match[1].split("\n")) {
143
+ const trimmed = line.trim();
144
+ if (!trimmed || trimmed.startsWith("#")) {
145
+ continue;
146
+ }
147
+ const descriptionMatch = trimmed.match(/^description\s*:\s*(.+)$/i);
148
+ if (descriptionMatch) {
149
+ description = stripWrappingQuotes(descriptionMatch[1]);
150
+ break;
151
+ }
152
+ }
153
+
154
+ return { description };
155
+ }
156
+
157
+ async function readJsonFile<T>(filePath: string): Promise<T | null> {
158
+ try {
159
+ const raw = await fs.readFile(filePath, "utf8");
160
+ return JSON.parse(raw) as T;
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ async function listFilesRecursive(dirPath: string): Promise<string[]> {
167
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
168
+ const nested = await Promise.all(
169
+ entries.map(async (entry) => {
170
+ if (entry.name === ".DS_Store") {
171
+ return [];
172
+ }
173
+
174
+ const fullPath = path.join(dirPath, entry.name);
175
+ if (entry.isDirectory()) {
176
+ return listFilesRecursive(fullPath);
177
+ }
178
+
179
+ if (entry.isFile()) {
180
+ return [fullPath];
181
+ }
182
+
183
+ return [];
184
+ }),
185
+ );
186
+
187
+ return nested.flat().sort();
188
+ }
189
+
190
+ async function computeFolderHash(dirPath: string): Promise<string> {
191
+ const hash = createHash("sha256");
192
+ const filePaths = await listFilesRecursive(dirPath);
193
+
194
+ for (const filePath of filePaths) {
195
+ const relativePath = path.relative(dirPath, filePath);
196
+ hash.update(relativePath);
197
+ hash.update("\0");
198
+ hash.update(await fs.readFile(filePath));
199
+ hash.update("\0");
200
+ }
201
+
202
+ return hash.digest("hex");
203
+ }
204
+
205
+ function resolveSkillBundleFilePath(
206
+ skillDir: string,
207
+ relativePath: string,
208
+ ): string {
209
+ const normalizedPath = assertRelativeBundlePath(relativePath);
210
+ const resolvedSkillDir = path.resolve(skillDir);
211
+ const candidatePath = path.resolve(resolvedSkillDir, normalizedPath);
212
+ if (
213
+ candidatePath === resolvedSkillDir ||
214
+ !isResolvedChildPath(resolvedSkillDir, candidatePath)
215
+ ) {
216
+ throw new Error(
217
+ `Bundle file resolves outside the expected directory: ${relativePath}`,
218
+ );
219
+ }
220
+ return candidatePath;
221
+ }
222
+
223
+ async function readSkillMdIfValid(skillDir: string): Promise<boolean> {
224
+ try {
225
+ await fs.readFile(path.join(skillDir, "SKILL.md"), "utf8");
226
+ return true;
227
+ } catch {
228
+ return false;
229
+ }
230
+ }
231
+
232
+ async function moveDirectory(
233
+ sourceDir: string,
234
+ destinationDir: string,
235
+ ): Promise<void> {
236
+ await fs.mkdir(path.dirname(destinationDir), { recursive: true });
237
+ try {
238
+ await fs.rename(sourceDir, destinationDir);
239
+ } catch (error) {
240
+ if ((error as NodeJS.ErrnoException).code !== "EXDEV") {
241
+ throw error;
242
+ }
243
+ await fs.cp(sourceDir, destinationDir, {
244
+ recursive: true,
245
+ errorOnExist: true,
246
+ force: false,
247
+ });
248
+ await fs.rm(sourceDir, { recursive: true, force: true });
249
+ }
250
+ }
251
+
252
+ async function createDirectorySymlink(
253
+ targetDir: string,
254
+ linkPath: string,
255
+ ): Promise<void> {
256
+ await fs.mkdir(path.dirname(linkPath), { recursive: true });
257
+ await fs.symlink(path.relative(path.dirname(linkPath), targetDir), linkPath, "dir");
258
+ }
259
+
260
+ function backupRootFor(paths: SkillSyncPaths, timestamp: string): string {
261
+ return path.join(paths.syncRoot, "skill-dedupe-backups", timestamp);
262
+ }
263
+
264
+ function relativeBackupPath(label: string, skillName: string): string {
265
+ return path.join(
266
+ label.replace(/[^A-Za-z0-9_.-]+/g, "_"),
267
+ safeName(skillName),
268
+ );
269
+ }
270
+
271
+ function isExcludedTopLevelSkillEntry(entryName: string): boolean {
272
+ return (
273
+ entryName.startsWith(".") ||
274
+ isTransientSkillDirectoryName(entryName) ||
275
+ EXCLUDED_TOP_LEVEL_ROOT_NAMES.has(entryName.toLowerCase())
276
+ );
277
+ }
278
+
279
+ function isTransientSkillDirectoryName(entryName: string): boolean {
280
+ return /\.(?:backup|staging)-/.test(entryName);
281
+ }
282
+
283
+ interface TopLevelSkillSource {
284
+ label: string;
285
+ root: string;
286
+ priority: number;
287
+ }
288
+
289
+ interface TopLevelSkillCandidate {
290
+ name: string;
291
+ root: string;
292
+ label: string;
293
+ path: string;
294
+ resolvedPath: string;
295
+ priority: number;
296
+ isScoped: boolean;
297
+ isSymlink: boolean;
298
+ }
299
+
300
+ export interface GatherTopLevelLocalSkillsOptions {
301
+ sourceRoots?: Array<{ label: string; root: string }>;
302
+ protectedSkillNames?: ReadonlySet<string>;
303
+ timestamp?: string;
304
+ }
305
+
306
+ export interface GatherTopLevelLocalSkillsResult {
307
+ gathered: number;
308
+ backedUp: number;
309
+ skipped: number;
310
+ metadataPath: string;
311
+ }
312
+
313
+ function defaultTopLevelSkillSources(
314
+ paths: SkillSyncPaths,
315
+ ): TopLevelSkillSource[] {
316
+ const sources: TopLevelSkillSource[] = [];
317
+ if (path.resolve(paths.legacyScopedSkillsDir) !== path.resolve(paths.skillsDir)) {
318
+ sources.push({
319
+ label: "notis-legacy-scoped",
320
+ root: paths.legacyScopedSkillsDir,
321
+ priority: 1,
322
+ });
323
+ }
324
+
325
+ sources.push(
326
+ { label: "agents", root: paths.legacySkillsDir, priority: 2 },
327
+ {
328
+ label: "codex",
329
+ root: path.join(HOME_DIR, ".codex", "skills"),
330
+ priority: 3,
331
+ },
332
+ {
333
+ label: "cursor",
334
+ root: path.join(HOME_DIR, ".cursor", "skills"),
335
+ priority: 4,
336
+ },
337
+ {
338
+ label: "claude",
339
+ root: path.join(HOME_DIR, ".claude", "skills"),
340
+ priority: 5,
341
+ },
342
+ );
343
+
344
+ return sources;
345
+ }
346
+
347
+ function isManagedTopLevelSymlinkTarget(
348
+ resolvedPath: string,
349
+ paths: SkillSyncPaths,
350
+ ): boolean {
351
+ const managedRoots = [
352
+ path.resolve(paths.skillsDir),
353
+ path.resolve(paths.legacySkillsDir),
354
+ path.resolve(paths.legacyScopedSkillsDir),
355
+ path.join(path.resolve(paths.syncRoot), "base"),
356
+ path.join(path.resolve(paths.syncRoot), "users"),
357
+ path.join(path.resolve(paths.agentsDir), "notis", "users"),
358
+ ];
359
+ return managedRoots.some(
360
+ (root) =>
361
+ resolvedPath === root || resolvedPath.startsWith(`${root}${path.sep}`),
362
+ );
363
+ }
364
+
365
+ async function listTopLevelSkillCandidates(
366
+ paths: SkillSyncPaths,
367
+ options: GatherTopLevelLocalSkillsOptions,
368
+ ): Promise<TopLevelSkillCandidate[]> {
369
+ const sourceRoots = options.sourceRoots
370
+ ? options.sourceRoots.map((source, index) => ({
371
+ ...source,
372
+ priority: index + 1,
373
+ }))
374
+ : defaultTopLevelSkillSources(paths);
375
+ const candidates: TopLevelSkillCandidate[] = [];
376
+
377
+ try {
378
+ const scopedEntries = await fs.readdir(paths.skillsDir, {
379
+ withFileTypes: true,
380
+ });
381
+ for (const entry of scopedEntries) {
382
+ if (
383
+ (!entry.isDirectory() && !entry.isSymbolicLink()) ||
384
+ entry.name.startsWith(".") ||
385
+ isTransientSkillDirectoryName(entry.name)
386
+ ) {
387
+ continue;
388
+ }
389
+ const skillDir = path.join(paths.skillsDir, entry.name);
390
+ let resolvedPath = path.resolve(skillDir);
391
+ if (entry.isSymbolicLink()) {
392
+ try {
393
+ resolvedPath = path.resolve(path.dirname(skillDir), await fs.readlink(skillDir));
394
+ } catch {
395
+ continue;
396
+ }
397
+ }
398
+ if (await readSkillMdIfValid(skillDir)) {
399
+ candidates.push({
400
+ name: safeName(entry.name, paths.skillsDir),
401
+ root: paths.skillsDir,
402
+ label: "notis-managed",
403
+ path: skillDir,
404
+ resolvedPath,
405
+ priority: 0,
406
+ isScoped: true,
407
+ isSymlink: entry.isSymbolicLink(),
408
+ });
409
+ }
410
+ }
411
+ } catch {
412
+ // Missing scoped mirror is expected before the first sync.
413
+ }
414
+
415
+ for (const source of sourceRoots) {
416
+ let entries: Dirent[];
417
+ try {
418
+ entries = await fs.readdir(source.root, { withFileTypes: true });
419
+ } catch {
420
+ continue;
421
+ }
422
+
423
+ for (const entry of entries) {
424
+ if (
425
+ isExcludedTopLevelSkillEntry(entry.name) ||
426
+ (!entry.isDirectory() && !entry.isSymbolicLink())
427
+ ) {
428
+ continue;
429
+ }
430
+
431
+ const candidatePath = path.join(source.root, entry.name);
432
+ let resolvedPath: string;
433
+ try {
434
+ resolvedPath = entry.isSymbolicLink()
435
+ ? path.resolve(
436
+ path.dirname(candidatePath),
437
+ await fs.readlink(candidatePath),
438
+ )
439
+ : path.resolve(candidatePath);
440
+ } catch {
441
+ continue;
442
+ }
443
+
444
+ if (
445
+ entry.isSymbolicLink() &&
446
+ isManagedTopLevelSymlinkTarget(resolvedPath, paths)
447
+ ) {
448
+ continue;
449
+ }
450
+
451
+ if (!(await readSkillMdIfValid(resolvedPath))) {
452
+ continue;
453
+ }
454
+
455
+ candidates.push({
456
+ name: safeName(entry.name, paths.skillsDir),
457
+ root: source.root,
458
+ label: source.label,
459
+ path: candidatePath,
460
+ resolvedPath,
461
+ priority: source.priority,
462
+ isScoped: false,
463
+ isSymlink: entry.isSymbolicLink(),
464
+ });
465
+ }
466
+ }
467
+
468
+ return candidates.sort(
469
+ (a, b) => a.priority - b.priority || a.name.localeCompare(b.name),
470
+ );
471
+ }
472
+
473
+ export async function gatherTopLevelLocalSkills(
474
+ paths: SkillSyncPaths,
475
+ options: GatherTopLevelLocalSkillsOptions = {},
476
+ ): Promise<GatherTopLevelLocalSkillsResult> {
477
+ await ensureCanonicalSkillsDir(paths);
478
+
479
+ const protectedSkillNames = options.protectedSkillNames || new Set<string>();
480
+ const timestamp =
481
+ options.timestamp || new Date().toISOString().replace(/[:.]/g, "-");
482
+ const backupRoot = backupRootFor(paths, timestamp);
483
+ const candidates = await listTopLevelSkillCandidates(paths, options);
484
+ const byName = new Map<string, TopLevelSkillCandidate[]>();
485
+
486
+ for (const candidate of candidates) {
487
+ const existing = byName.get(candidate.name) || [];
488
+ existing.push(candidate);
489
+ byName.set(candidate.name, existing);
490
+ }
491
+
492
+ let gathered = 0;
493
+ let backedUp = 0;
494
+ let skipped = 0;
495
+ const metadata: {
496
+ version: 1;
497
+ gatheredAt: string;
498
+ skills: Record<
499
+ string,
500
+ {
501
+ canonicalPath: string | null;
502
+ skippedSources: string[];
503
+ protectedFromLocalGather?: boolean;
504
+ }
505
+ >;
506
+ } = {
507
+ version: 1,
508
+ gatheredAt: new Date().toISOString(),
509
+ skills: {},
510
+ };
511
+
512
+ for (const [skillName, skillCandidates] of byName) {
513
+ const protectedFromLocalGather = protectedSkillNames.has(skillName);
514
+ const canonical = protectedFromLocalGather
515
+ ? skillCandidates.find((candidate) => candidate.isScoped) || null
516
+ : skillCandidates[0];
517
+ const destinationName = safeName(skillName, paths.skillsDir);
518
+ const destinationDir = path.join(paths.skillsDir, destinationName);
519
+ const skippedSources: string[] = [];
520
+
521
+ if (!canonical) {
522
+ for (const candidate of skillCandidates) {
523
+ if (candidate.isSymlink) {
524
+ skipped += 1;
525
+ skippedSources.push(candidate.path);
526
+ continue;
527
+ }
528
+ const backupDir = path.join(
529
+ backupRoot,
530
+ relativeBackupPath(candidate.label, skillName),
531
+ );
532
+ try {
533
+ await moveDirectory(candidate.path, backupDir);
534
+ backedUp += 1;
535
+ skippedSources.push(candidate.path);
536
+ } catch {
537
+ skipped += 1;
538
+ }
539
+ }
540
+ metadata.skills[skillName] = {
541
+ canonicalPath: null,
542
+ skippedSources,
543
+ protectedFromLocalGather: true,
544
+ };
545
+ continue;
546
+ }
547
+
548
+ if (!canonical.isScoped && !(await pathExists(destinationDir))) {
549
+ try {
550
+ if (canonical.isSymlink) {
551
+ await createDirectorySymlink(canonical.resolvedPath, destinationDir);
552
+ } else {
553
+ await moveDirectory(canonical.path, destinationDir);
554
+ }
555
+ gathered += 1;
556
+ } catch {
557
+ skipped += 1;
558
+ }
559
+ }
560
+
561
+ for (const candidate of skillCandidates) {
562
+ if (candidate === canonical || candidate.isScoped) {
563
+ continue;
564
+ }
565
+ if (candidate.isSymlink) {
566
+ skippedSources.push(candidate.path);
567
+ continue;
568
+ }
569
+ const backupDir = path.join(
570
+ backupRoot,
571
+ relativeBackupPath(candidate.label, skillName),
572
+ );
573
+ try {
574
+ await moveDirectory(candidate.path, backupDir);
575
+ backedUp += 1;
576
+ skippedSources.push(candidate.path);
577
+ } catch {
578
+ skipped += 1;
579
+ }
580
+ }
581
+
582
+ metadata.skills[skillName] = {
583
+ canonicalPath: (await pathExists(destinationDir))
584
+ ? destinationDir
585
+ : canonical.path,
586
+ skippedSources,
587
+ ...(protectedFromLocalGather ? { protectedFromLocalGather: true } : {}),
588
+ };
589
+ }
590
+
591
+ await fs.mkdir(path.dirname(paths.gatherMetadataPath), { recursive: true });
592
+ await fs.writeFile(
593
+ paths.gatherMetadataPath,
594
+ `${JSON.stringify(metadata, null, 2)}\n`,
595
+ "utf8",
596
+ );
597
+
598
+ return {
599
+ gathered,
600
+ backedUp,
601
+ skipped,
602
+ metadataPath: paths.gatherMetadataPath,
603
+ };
604
+ }
605
+
606
+ async function readSkillLock(
607
+ paths: SkillSyncPaths = DEFAULT_SYNC_PATHS,
608
+ ): Promise<Record<string, string>> {
609
+ type SkillLock = {
610
+ skills?: Record<string, { sourceUrl?: string }>;
611
+ };
612
+
613
+ const lockData = await readJsonFile<SkillLock>(paths.skillLockPath);
614
+ const sourceUrls: Record<string, string> = {};
615
+
616
+ for (const [skillName, entry] of Object.entries(lockData?.skills || {})) {
617
+ if (typeof entry?.sourceUrl === "string" && entry.sourceUrl.trim()) {
618
+ sourceUrls[skillName] = entry.sourceUrl.trim();
619
+ }
620
+ }
621
+
622
+ return sourceUrls;
623
+ }
624
+
625
+ export async function ensureCanonicalSkillsDir(
626
+ paths: SkillSyncPaths = DEFAULT_SYNC_PATHS,
627
+ ): Promise<void> {
628
+ await fs.mkdir(paths.skillsDir, { recursive: true });
629
+ }
630
+
631
+ export async function initializeScopedSkillsFromLegacy(
632
+ paths: SkillSyncPaths,
633
+ ): Promise<number> {
634
+ const usingLegacyPath =
635
+ path.resolve(paths.skillsDir) === path.resolve(paths.legacySkillsDir);
636
+ if (usingLegacyPath || (await pathExists(paths.syncStatePath))) {
637
+ await ensureCanonicalSkillsDir(paths);
638
+ return 0;
639
+ }
640
+
641
+ await ensureCanonicalSkillsDir(paths);
642
+ let entries: Dirent[];
643
+ try {
644
+ entries = await fs.readdir(paths.legacySkillsDir, { withFileTypes: true });
645
+ } catch {
646
+ return 0;
647
+ }
648
+
649
+ let copied = 0;
650
+ for (const entry of entries) {
651
+ if (!entry.isDirectory()) {
652
+ continue;
653
+ }
654
+
655
+ const legacySkillDir = path.join(paths.legacySkillsDir, entry.name);
656
+ const targetSkillName = safeName(entry.name, paths.skillsDir);
657
+ const scopedSkillDir = path.join(paths.skillsDir, targetSkillName);
658
+ try {
659
+ await fs.readFile(path.join(legacySkillDir, "SKILL.md"), "utf8");
660
+ if (await pathExists(scopedSkillDir)) {
661
+ continue;
662
+ }
663
+ await fs.cp(legacySkillDir, scopedSkillDir, {
664
+ recursive: true,
665
+ errorOnExist: true,
666
+ force: false,
667
+ });
668
+ copied += 1;
669
+ } catch {
670
+ // Ignore invalid legacy skill folders and existing scoped copies.
671
+ }
672
+ }
673
+
674
+ return copied;
675
+ }
676
+
677
+ export async function scanLocalSkills(
678
+ paths: SkillSyncPaths = DEFAULT_SYNC_PATHS,
679
+ ): Promise<LocalSkill[]> {
680
+ await ensureCanonicalSkillsDir(paths);
681
+
682
+ const sourceUrls = await readSkillLock(paths);
683
+ const entries = await fs.readdir(paths.skillsDir, { withFileTypes: true });
684
+ const skills: Array<LocalSkill | null> = await Promise.all(
685
+ entries.map(async (entry) => {
686
+ if (
687
+ (!entry.isDirectory() && !entry.isSymbolicLink()) ||
688
+ isTransientSkillDirectoryName(entry.name)
689
+ ) {
690
+ return null;
691
+ }
692
+
693
+ const skillDir = path.join(paths.skillsDir, entry.name);
694
+ const skillMdPath = path.join(skillDir, "SKILL.md");
695
+
696
+ try {
697
+ const skillMd = await fs.readFile(skillMdPath, "utf8");
698
+ const { description } = parseFrontMatter(skillMd);
699
+ const folderHash = await computeFolderHash(skillDir);
700
+ const skill: LocalSkill = {
701
+ name: entry.name,
702
+ skillMd,
703
+ description,
704
+ folderHash,
705
+ directoryPath: skillDir,
706
+ };
707
+ if (sourceUrls[entry.name]) {
708
+ skill.sourceUrl = sourceUrls[entry.name];
709
+ }
710
+ return skill;
711
+ } catch {
712
+ return null;
713
+ }
714
+ }),
715
+ );
716
+
717
+ return skills
718
+ .filter((skill): skill is LocalSkill => skill !== null)
719
+ .sort((a, b) => a.name.localeCompare(b.name));
720
+ }
721
+
722
+ function normalizeSyncState(
723
+ state: NotisSyncState | null,
724
+ ): NotisSyncState | null {
725
+ if (!state || state.version !== 1 || typeof state.skills !== "object") {
726
+ return null;
727
+ }
728
+ const normalizeStoredAgentTargets = (
729
+ targets: Partial<NotisSyncState["skills"][string]["agentTargets"]> | null | undefined,
730
+ ): NotisSyncState["skills"][string]["agentTargets"] => ({
731
+ notis: Boolean(targets?.notis ?? true),
732
+ claude_code: Boolean(targets?.claude_code ?? true),
733
+ cursor: Boolean(targets?.cursor ?? true),
734
+ codex: Boolean(targets?.codex ?? true),
735
+ });
736
+
737
+ return {
738
+ version: 1,
739
+ lastSyncedAt:
740
+ typeof state.lastSyncedAt === "string" ? state.lastSyncedAt : null,
741
+ skills: Object.fromEntries(
742
+ Object.entries(state.skills || {}).map(([skillName, skillState]) => [
743
+ skillName,
744
+ {
745
+ ...skillState,
746
+ agentTargets: normalizeStoredAgentTargets(skillState.agentTargets),
747
+ },
748
+ ]),
749
+ ),
750
+ };
751
+ }
752
+
753
+ export async function readSyncState(
754
+ paths: SkillSyncPaths = DEFAULT_SYNC_PATHS,
755
+ ): Promise<NotisSyncState> {
756
+ const scopedState = normalizeSyncState(
757
+ await readJsonFile<NotisSyncState>(paths.syncStatePath),
758
+ );
759
+ if (scopedState) {
760
+ return scopedState;
761
+ }
762
+
763
+ return DEFAULT_SYNC_STATE;
764
+ }
765
+
766
+ export async function readLegacySyncState(
767
+ paths: SkillSyncPaths = DEFAULT_SYNC_PATHS,
768
+ ): Promise<NotisSyncState | null> {
769
+ const legacyStatePaths = [
770
+ paths.legacyScopedSyncStatePath,
771
+ path.resolve(paths.syncStatePath) === path.resolve(LEGACY_NOTIS_SYNC_STATE_PATH)
772
+ ? paths.syncStatePath
773
+ : path.join(paths.agentsDir, ".notis-sync.json"),
774
+ ];
775
+
776
+ for (const legacyStatePath of legacyStatePaths) {
777
+ const state = normalizeSyncState(
778
+ await readJsonFile<NotisSyncState>(legacyStatePath),
779
+ );
780
+ if (state) {
781
+ return state;
782
+ }
783
+ }
784
+
785
+ return null;
786
+ }
787
+
788
+ export async function writeSyncState(
789
+ state: NotisSyncState,
790
+ paths: SkillSyncPaths = DEFAULT_SYNC_PATHS,
791
+ ): Promise<void> {
792
+ await fs.mkdir(path.dirname(paths.syncStatePath), { recursive: true });
793
+ await fs.writeFile(
794
+ paths.syncStatePath,
795
+ `${JSON.stringify(state, null, 2)}\n`,
796
+ "utf8",
797
+ );
798
+ }
799
+
800
+ export function safeName(
801
+ name: string,
802
+ baseDir: string = AGENTS_SKILLS_DIR,
803
+ ): string {
804
+ const sanitizedName = name
805
+ .trim()
806
+ .replace(/[\\/]+/g, "")
807
+ .replace(/\.\./g, "");
808
+ if (!sanitizedName) {
809
+ throw new Error("Invalid skill name");
810
+ }
811
+
812
+ const resolvedBaseDir = path.resolve(baseDir);
813
+ const resolvedPath = path.resolve(resolvedBaseDir, sanitizedName);
814
+ if (!isResolvedChildPath(resolvedBaseDir, resolvedPath)) {
815
+ throw new Error("Skill name resolves outside the expected directory");
816
+ }
817
+
818
+ return sanitizedName;
819
+ }
820
+
821
+ export async function deleteLocalSkill(
822
+ skillName: string,
823
+ paths: SkillSyncPaths = DEFAULT_SYNC_PATHS,
824
+ ): Promise<boolean> {
825
+ const skillDir = path.join(
826
+ paths.skillsDir,
827
+ safeName(skillName, paths.skillsDir),
828
+ );
829
+ try {
830
+ await fs.rm(skillDir, { recursive: true, force: true });
831
+ return true;
832
+ } catch {
833
+ return false;
834
+ }
835
+ }
836
+
837
+ async function createZipFromDirectory(directoryPath: string): Promise<string> {
838
+ const zipPath = path.join(
839
+ os.tmpdir(),
840
+ `notis-skill-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`,
841
+ );
842
+ const parentDir = path.dirname(directoryPath);
843
+ const directoryName = path.basename(directoryPath);
844
+
845
+ if (process.platform === "win32") {
846
+ await execFileAsync("powershell.exe", [
847
+ "-NoProfile",
848
+ "-Command",
849
+ `Compress-Archive -Path '${directoryPath.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`,
850
+ ]);
851
+ return zipPath;
852
+ }
853
+
854
+ await execFileAsync("zip", ["-qry", zipPath, directoryName], {
855
+ cwd: parentDir,
856
+ });
857
+ return zipPath;
858
+ }
859
+
860
+ async function extractZipToDirectory(
861
+ zipPath: string,
862
+ destinationDir: string,
863
+ ): Promise<void> {
864
+ const extractRoot = await fs.mkdtemp(
865
+ path.join(os.tmpdir(), "notis-skill-extract-"),
866
+ );
867
+
868
+ try {
869
+ if (process.platform === "win32") {
870
+ await execFileAsync("powershell.exe", [
871
+ "-NoProfile",
872
+ "-Command",
873
+ `Expand-Archive -Path '${zipPath.replace(/'/g, "''")}' -DestinationPath '${extractRoot.replace(/'/g, "''")}' -Force`,
874
+ ]);
875
+ } else {
876
+ await execFileAsync("unzip", ["-qq", zipPath, "-d", extractRoot]);
877
+ }
878
+
879
+ const extractedEntries = await fs.readdir(extractRoot, {
880
+ withFileTypes: true,
881
+ });
882
+ const extractedDirectory = extractedEntries.find((entry) =>
883
+ entry.isDirectory(),
884
+ );
885
+ const sourceDir = extractedDirectory
886
+ ? path.join(extractRoot, extractedDirectory.name)
887
+ : extractRoot;
888
+
889
+ await fs.rm(destinationDir, { recursive: true, force: true });
890
+ await fs.mkdir(path.dirname(destinationDir), { recursive: true });
891
+ await fs.cp(sourceDir, destinationDir, { recursive: true });
892
+ } finally {
893
+ await fs.rm(extractRoot, { recursive: true, force: true });
894
+ }
895
+ }
896
+
897
+ async function pathExists(targetPath: string): Promise<boolean> {
898
+ try {
899
+ await fs.access(targetPath);
900
+ return true;
901
+ } catch {
902
+ return false;
903
+ }
904
+ }
905
+
906
+ async function replaceSkillDirectoryAtomically(
907
+ skillDir: string,
908
+ populateDir: (stagingDir: string) => Promise<void>,
909
+ ): Promise<void> {
910
+ const parentDir = path.dirname(skillDir);
911
+ const skillName = path.basename(skillDir);
912
+ await fs.mkdir(parentDir, { recursive: true });
913
+
914
+ const stagingDir = await fs.mkdtemp(
915
+ path.join(parentDir, `${skillName}.staging-`),
916
+ );
917
+ const backupDir = path.join(
918
+ parentDir,
919
+ `${skillName}.backup-${Date.now()}-${Math.random().toString(36).slice(2)}`,
920
+ );
921
+
922
+ let movedExisting = false;
923
+ let promotedStaging = false;
924
+ let cleanupError: Error | null = null;
925
+
926
+ try {
927
+ await populateDir(stagingDir);
928
+
929
+ if (await pathExists(skillDir)) {
930
+ await fs.rename(skillDir, backupDir);
931
+ movedExisting = true;
932
+ }
933
+
934
+ await fs.rename(stagingDir, skillDir);
935
+ promotedStaging = true;
936
+ } catch (error) {
937
+ if (movedExisting && !promotedStaging && (await pathExists(backupDir))) {
938
+ await fs.rename(backupDir, skillDir);
939
+ }
940
+ throw error;
941
+ } finally {
942
+ if (!promotedStaging && (await pathExists(stagingDir))) {
943
+ await fs.rm(stagingDir, { recursive: true, force: true });
944
+ }
945
+ if ((promotedStaging || !movedExisting) && (await pathExists(backupDir))) {
946
+ try {
947
+ await fs.rm(backupDir, { recursive: true, force: true });
948
+ } catch (error) {
949
+ if (!cleanupError) {
950
+ cleanupError =
951
+ error instanceof Error ? error : new Error(String(error));
952
+ }
953
+ }
954
+ }
955
+ if (cleanupError) {
956
+ console.warn(
957
+ `[Notis] Failed to clean up temporary skill directory backup for "${skillDir}"`,
958
+ cleanupError,
959
+ );
960
+ }
961
+ }
962
+ }
963
+
964
+ function bundleFilesIncludeSkillMd(
965
+ bundleFiles: Array<{ path: string }>,
966
+ ): boolean {
967
+ return bundleFiles.some((bundleFile) => {
968
+ const basename = path.basename(bundleFile.path).toLowerCase();
969
+ return basename === "skill.md" || basename === "skills.md";
970
+ });
971
+ }
972
+
973
+ export async function createSkillBundleBase64(
974
+ skill: LocalSkill,
975
+ ): Promise<string> {
976
+ const zipPath = await createZipFromDirectory(skill.directoryPath);
977
+ try {
978
+ const zipBytes = await fs.readFile(zipPath);
979
+ return zipBytes.toString("base64");
980
+ } finally {
981
+ await fs.rm(zipPath, { force: true });
982
+ }
983
+ }
984
+
985
+ export async function writeCloudSkillToDisk(
986
+ skill: CloudSkill,
987
+ bundleBytes?: Buffer,
988
+ paths: SkillSyncPaths = DEFAULT_SYNC_PATHS,
989
+ ): Promise<boolean> {
990
+ const skillDir = path.join(
991
+ paths.skillsDir,
992
+ safeName(skill.name, paths.skillsDir),
993
+ );
994
+
995
+ if (bundleBytes?.length) {
996
+ const bundlePath = path.join(
997
+ os.tmpdir(),
998
+ `notis-skill-download-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`,
999
+ );
1000
+ try {
1001
+ await fs.writeFile(bundlePath, bundleBytes);
1002
+ await extractZipToDirectory(bundlePath, skillDir);
1003
+ return true;
1004
+ } finally {
1005
+ await fs.rm(bundlePath, { force: true });
1006
+ }
1007
+ }
1008
+
1009
+ if (
1010
+ !skill.skill_md &&
1011
+ (!skill.bundle_files || skill.bundle_files.length === 0)
1012
+ ) {
1013
+ return false;
1014
+ }
1015
+
1016
+ if (skill.bundle_files && skill.bundle_files.length > 0) {
1017
+ if (!bundleFilesIncludeSkillMd(skill.bundle_files)) {
1018
+ throw new Error(
1019
+ `Synced bundle for "${skill.name}" is missing SKILL.md or SKILLS.md`,
1020
+ );
1021
+ }
1022
+ await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
1023
+ for (const bundleFile of skill.bundle_files || []) {
1024
+ const filePath = resolveSkillBundleFilePath(
1025
+ stagingDir,
1026
+ bundleFile.path,
1027
+ );
1028
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
1029
+ await fs.writeFile(
1030
+ filePath,
1031
+ Buffer.from(bundleFile.content_b64, "base64"),
1032
+ );
1033
+ }
1034
+ });
1035
+ return true;
1036
+ }
1037
+
1038
+ await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
1039
+ await fs.writeFile(
1040
+ path.join(stagingDir, "SKILL.md"),
1041
+ skill.skill_md ?? "",
1042
+ "utf8",
1043
+ );
1044
+ });
1045
+ return true;
1046
+ }