@notis_ai/cli 0.2.0-beta.137.1 → 0.2.0-beta.144.1

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 (294) 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/MarkdownEditor.tsx +121 -0
  50. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +37 -119
  51. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
  52. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
  53. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
  54. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  55. package/template/packages/sdk/src/config.ts +19 -0
  56. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  57. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +709 -0
  58. package/template/packages/sdk/src/hooks/useHandover.ts +3 -0
  59. package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
  60. package/template/packages/sdk/src/hooks/useTool.ts +5 -4
  61. package/template/packages/sdk/src/index.ts +47 -2
  62. package/template/packages/sdk/src/interactions/actions.ts +46 -0
  63. package/template/packages/sdk/src/interactions/shortcuts.tsx +628 -0
  64. package/template/packages/sdk/src/interactions.ts +41 -0
  65. package/template/packages/sdk/src/provider.tsx +2 -1
  66. package/template/packages/sdk/src/runtime.ts +85 -3
  67. package/dist/scaffolds/notis-database/CHANGELOG.md +0 -5
  68. package/dist/scaffolds/notis-database/app/globals.css +0 -44
  69. package/dist/scaffolds/notis-database/app/layout.tsx +0 -6
  70. package/dist/scaffolds/notis-database/app/page.tsx +0 -1088
  71. package/dist/scaffolds/notis-database/components/ui/badge.tsx +0 -28
  72. package/dist/scaffolds/notis-database/components/ui/button.tsx +0 -53
  73. package/dist/scaffolds/notis-database/components/ui/card.tsx +0 -56
  74. package/dist/scaffolds/notis-database/components/ui/table.tsx +0 -120
  75. package/dist/scaffolds/notis-database/components.json +0 -20
  76. package/dist/scaffolds/notis-database/index.html +0 -12
  77. package/dist/scaffolds/notis-database/lib/types.ts +0 -132
  78. package/dist/scaffolds/notis-database/lib/utils.ts +0 -6
  79. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  80. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  81. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  82. package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
  83. package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
  84. package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +0 -1839
  85. package/dist/scaffolds/notis-database/notis.config.ts +0 -73
  86. package/dist/scaffolds/notis-database/package-lock.json +0 -3935
  87. package/dist/scaffolds/notis-database/package.json +0 -32
  88. package/dist/scaffolds/notis-database/packages/sdk/package.json +0 -36
  89. package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  90. package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +0 -60
  91. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  92. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  93. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  94. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +0 -234
  95. package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -250
  96. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
  97. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  98. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  99. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  100. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +0 -78
  101. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +0 -121
  102. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useHandover.ts +0 -75
  103. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  104. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +0 -34
  105. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  106. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +0 -64
  107. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +0 -56
  108. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  109. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  110. package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +0 -100
  111. package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
  112. package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -351
  113. package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +0 -186
  114. package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +0 -15
  115. package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +0 -56
  116. package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +0 -15
  117. package/dist/scaffolds/notis-database/postcss.config.mjs +0 -8
  118. package/dist/scaffolds/notis-database/src/dev-main.tsx +0 -23
  119. package/dist/scaffolds/notis-database/src/mock-runtime.ts +0 -561
  120. package/dist/scaffolds/notis-database/tailwind.config.ts +0 -59
  121. package/dist/scaffolds/notis-database/tsconfig.json +0 -23
  122. package/dist/scaffolds/notis-database/vite.config.ts +0 -22
  123. package/dist/scaffolds/notis-journal/CHANGELOG.md +0 -29
  124. package/dist/scaffolds/notis-journal/app/globals.css +0 -37
  125. package/dist/scaffolds/notis-journal/app/insights/page.tsx +0 -513
  126. package/dist/scaffolds/notis-journal/app/journal-core.tsx +0 -362
  127. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +0 -337
  128. package/dist/scaffolds/notis-journal/app/layout.tsx +0 -6
  129. package/dist/scaffolds/notis-journal/app/page.tsx +0 -486
  130. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +0 -28
  131. package/dist/scaffolds/notis-journal/components/ui/button.tsx +0 -53
  132. package/dist/scaffolds/notis-journal/components/ui/card.tsx +0 -56
  133. package/dist/scaffolds/notis-journal/components.json +0 -20
  134. package/dist/scaffolds/notis-journal/index.html +0 -12
  135. package/dist/scaffolds/notis-journal/lib/utils.ts +0 -6
  136. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  137. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  138. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  139. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  140. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  141. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  142. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +0 -132
  143. package/dist/scaffolds/notis-journal/notis.config.ts +0 -93
  144. package/dist/scaffolds/notis-journal/package-lock.json +0 -4615
  145. package/dist/scaffolds/notis-journal/package.json +0 -34
  146. package/dist/scaffolds/notis-journal/packages/sdk/package.json +0 -36
  147. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  148. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +0 -60
  149. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  150. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  151. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  152. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +0 -234
  153. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -250
  154. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
  155. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  156. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  157. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  158. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +0 -78
  159. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +0 -121
  160. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useHandover.ts +0 -75
  161. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  162. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +0 -34
  163. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  164. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +0 -64
  165. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +0 -56
  166. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  167. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  168. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +0 -100
  169. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
  170. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -351
  171. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +0 -186
  172. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +0 -15
  173. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +0 -56
  174. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +0 -15
  175. package/dist/scaffolds/notis-journal/postcss.config.mjs +0 -8
  176. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +0 -120
  177. package/dist/scaffolds/notis-journal/src/dev-main.tsx +0 -58
  178. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +0 -199
  179. package/dist/scaffolds/notis-journal/tailwind.config.ts +0 -58
  180. package/dist/scaffolds/notis-journal/tsconfig.json +0 -23
  181. package/dist/scaffolds/notis-journal/vite.config.ts +0 -10
  182. package/dist/scaffolds/notis-notes/CHANGELOG.md +0 -5
  183. package/dist/scaffolds/notis-notes/app/globals.css +0 -3
  184. package/dist/scaffolds/notis-notes/app/layout.tsx +0 -6
  185. package/dist/scaffolds/notis-notes/app/page.tsx +0 -1943
  186. package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +0 -596
  187. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +0 -28
  188. package/dist/scaffolds/notis-notes/components/ui/button.tsx +0 -53
  189. package/dist/scaffolds/notis-notes/components/ui/card.tsx +0 -56
  190. package/dist/scaffolds/notis-notes/components.json +0 -20
  191. package/dist/scaffolds/notis-notes/lib/utils.ts +0 -6
  192. package/dist/scaffolds/notis-notes/lib/visible-properties.ts +0 -144
  193. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  194. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  195. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  196. package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
  197. package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
  198. package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +0 -752
  199. package/dist/scaffolds/notis-notes/notis.config.ts +0 -80
  200. package/dist/scaffolds/notis-notes/package-lock.json +0 -4636
  201. package/dist/scaffolds/notis-notes/package.json +0 -35
  202. package/dist/scaffolds/notis-notes/packages/sdk/package.json +0 -36
  203. package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  204. package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +0 -60
  205. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  206. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  207. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  208. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +0 -234
  209. package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -250
  210. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
  211. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  212. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  213. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  214. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +0 -78
  215. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +0 -121
  216. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useHandover.ts +0 -75
  217. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  218. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +0 -34
  219. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  220. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +0 -64
  221. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +0 -56
  222. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  223. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  224. package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +0 -100
  225. package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
  226. package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -351
  227. package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +0 -186
  228. package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +0 -15
  229. package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +0 -56
  230. package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +0 -15
  231. package/dist/scaffolds/notis-notes/postcss.config.mjs +0 -8
  232. package/dist/scaffolds/notis-notes/tailwind.config.ts +0 -58
  233. package/dist/scaffolds/notis-notes/tsconfig.json +0 -23
  234. package/dist/scaffolds/notis-notes/vite.config.ts +0 -10
  235. package/dist/scaffolds/notis-random/CHANGELOG.md +0 -15
  236. package/dist/scaffolds/notis-random/README.md +0 -33
  237. package/dist/scaffolds/notis-random/app/globals.css +0 -11
  238. package/dist/scaffolds/notis-random/app/history/page.tsx +0 -67
  239. package/dist/scaffolds/notis-random/app/layout.tsx +0 -7
  240. package/dist/scaffolds/notis-random/app/page.tsx +0 -289
  241. package/dist/scaffolds/notis-random/components/ui/button.tsx +0 -50
  242. package/dist/scaffolds/notis-random/components/ui/card.tsx +0 -16
  243. package/dist/scaffolds/notis-random/components/ui/input.tsx +0 -23
  244. package/dist/scaffolds/notis-random/components.json +0 -20
  245. package/dist/scaffolds/notis-random/index.html +0 -12
  246. package/dist/scaffolds/notis-random/lib/notis-tools.ts +0 -128
  247. package/dist/scaffolds/notis-random/lib/rng.ts +0 -202
  248. package/dist/scaffolds/notis-random/lib/roll-record.ts +0 -189
  249. package/dist/scaffolds/notis-random/lib/utils.ts +0 -25
  250. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  251. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  252. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  253. package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
  254. package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
  255. package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +0 -753
  256. package/dist/scaffolds/notis-random/notis.config.ts +0 -86
  257. package/dist/scaffolds/notis-random/package-lock.json +0 -4513
  258. package/dist/scaffolds/notis-random/package.json +0 -36
  259. package/dist/scaffolds/notis-random/packages/sdk/package.json +0 -36
  260. package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  261. package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +0 -60
  262. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  263. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  264. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  265. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +0 -234
  266. package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -250
  267. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
  268. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  269. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  270. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  271. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +0 -78
  272. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +0 -121
  273. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useHandover.ts +0 -75
  274. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  275. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +0 -34
  276. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  277. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +0 -64
  278. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +0 -56
  279. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  280. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  281. package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +0 -100
  282. package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
  283. package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -351
  284. package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +0 -186
  285. package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +0 -15
  286. package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +0 -56
  287. package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +0 -15
  288. package/dist/scaffolds/notis-random/postcss.config.mjs +0 -6
  289. package/dist/scaffolds/notis-random/src/dev-main.tsx +0 -70
  290. package/dist/scaffolds/notis-random/src/mock-runtime.ts +0 -129
  291. package/dist/scaffolds/notis-random/tailwind.config.ts +0 -50
  292. package/dist/scaffolds/notis-random/tsconfig.json +0 -23
  293. package/dist/scaffolds/notis-random/vite.config.ts +0 -11
  294. 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
+ }