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