@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,367 @@
1
+ /**
2
+ * Store-backed scaffold catalog for the Notis CLI.
3
+ *
4
+ * Every app published to the public Store lives with its full source in the
5
+ * public registry repository (github.com/mindtheflo/notis-apps, one app per
6
+ * `apps/<slug>/` directory). That repository IS the scaffold catalog:
7
+ * `apps scaffolds list` reads the published listings and `apps init --from
8
+ * <slug>` downloads the listed app's source. Publishing an app automatically
9
+ * makes it a scaffold — the CLI bundles nothing besides the bare template.
10
+ */
11
+
12
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
13
+ import { tmpdir } from 'node:os';
14
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
15
+
16
+ import { usageError } from './errors.js';
17
+
18
+ const DEFAULT_REGISTRY_REPO = 'mindtheflo/notis-apps';
19
+ const DEFAULT_REGISTRY_REF = 'main';
20
+ // How many registry files to download at once when materializing a scaffold.
21
+ const DOWNLOAD_CONCURRENCY = 4;
22
+ const MAX_SCAFFOLD_FILES = 2_000;
23
+ const MAX_SCAFFOLD_FILE_BYTES = 5 * 1024 * 1024;
24
+ const MAX_SCAFFOLD_TOTAL_BYTES = 50 * 1024 * 1024;
25
+ const MAX_CATALOG_ENTRIES = 1_000;
26
+ const MAX_REGISTRY_COMMIT_BYTES = 2 * 1024 * 1024;
27
+ const MAX_REGISTRY_API_BYTES = 10 * 1024 * 1024;
28
+ const MAX_CATALOG_METADATA_BYTES = 1024 * 1024;
29
+ const MAX_CATALOG_TOTAL_BYTES = 20 * 1024 * 1024;
30
+ // Registry bookkeeping that must never enter a new project: the listing
31
+ // descriptor and the rendered Store gallery describe the published app, and a
32
+ // scaffold copy starts a new identity.
33
+ const REGISTRY_ARTIFACT = /^(notis-listing\.json$|screenshots(\/|$))/;
34
+ // Files the scaffold copy step would drop anyway — skip the download.
35
+ const SKIPPED_SOURCE = /(^|\/)(node_modules|\.notis|\.git|dist|coverage|\.next|\.turbo)(\/|$)|(^|\/)\.env|\.(test|spec)\.[cm]?[jt]sx?$/;
36
+
37
+ function registryRepo() {
38
+ return process.env.NOTIS_APP_REGISTRY_REPO || DEFAULT_REGISTRY_REPO;
39
+ }
40
+
41
+ function registryRef() {
42
+ return process.env.NOTIS_APP_REGISTRY_REF || DEFAULT_REGISTRY_REF;
43
+ }
44
+
45
+ /**
46
+ * Local registry checkout override (`<dir>/apps/<slug>/…`). Used by tests and
47
+ * by offline development against a clone of the registry repository.
48
+ */
49
+ function localRegistryDir() {
50
+ return process.env.NOTIS_APP_REGISTRY_DIR || null;
51
+ }
52
+
53
+ export function scaffoldRegistryLabel() {
54
+ return localRegistryDir() || `github.com/${registryRepo()}`;
55
+ }
56
+
57
+ async function fetchRegistry(url, { binary = false, maxBytes = null } = {}) {
58
+ let response;
59
+ try {
60
+ response = await fetch(url, { headers: { 'user-agent': 'notis-cli' } });
61
+ } catch (error) {
62
+ throw usageError(
63
+ `Could not reach the Notis app registry (${scaffoldRegistryLabel()}): ${error.message}. ` +
64
+ 'Scaffolds are downloaded from published Store apps and need network access.',
65
+ );
66
+ }
67
+ if (!response.ok) {
68
+ throw usageError(`Notis app registry request failed (${response.status}) for ${url}.`);
69
+ }
70
+ const contentLength = Number(response.headers?.get?.('content-length'));
71
+ if (maxBytes && Number.isFinite(contentLength) && contentLength > maxBytes) {
72
+ throw usageError(`Notis app registry response exceeds the ${maxBytes}-byte limit.`);
73
+ }
74
+ let data;
75
+ if (maxBytes && response.body?.getReader) {
76
+ const reader = response.body.getReader();
77
+ const chunks = [];
78
+ let total = 0;
79
+ while (true) {
80
+ const { done, value } = await reader.read();
81
+ if (done) break;
82
+ const chunk = Buffer.from(value);
83
+ total += chunk.length;
84
+ if (total > maxBytes) {
85
+ await reader.cancel();
86
+ throw usageError(`Notis app registry response exceeds the ${maxBytes}-byte limit.`);
87
+ }
88
+ chunks.push(chunk);
89
+ }
90
+ data = Buffer.concat(chunks, total);
91
+ } else {
92
+ data = Buffer.from(await response.arrayBuffer());
93
+ }
94
+ if (maxBytes && data.length > maxBytes) {
95
+ throw usageError(`Notis app registry response exceeds the ${maxBytes}-byte limit.`);
96
+ }
97
+ return binary ? data : data.toString('utf8');
98
+ }
99
+
100
+ function encodedRegistryRepoPath() {
101
+ const parts = registryRepo().split('/');
102
+ if (
103
+ parts.length !== 2
104
+ || parts.some((part) => !part || !/^[A-Za-z0-9_.-]+$/.test(part))
105
+ ) {
106
+ throw usageError('Notis app registry repository must use the owner/repository form.');
107
+ }
108
+ return parts.map((part) => encodeURIComponent(part)).join('/');
109
+ }
110
+
111
+ async function resolveRegistryCommit() {
112
+ const url = `https://api.github.com/repos/${encodedRegistryRepoPath()}/commits/${encodeURIComponent(registryRef())}`;
113
+ // GitHub's commit-detail response includes changed-file metadata and can be
114
+ // substantially larger than the SHA we read from it. Keep a bounded limit,
115
+ // but allow normal registry commits with a long file list.
116
+ const payload = JSON.parse(await fetchRegistry(url, { maxBytes: MAX_REGISTRY_COMMIT_BYTES }));
117
+ const commitSha = typeof payload?.sha === 'string' ? payload.sha.trim() : '';
118
+ if (!/^[0-9a-f]{40}$/i.test(commitSha)) {
119
+ throw usageError(
120
+ `Notis app registry returned no immutable commit for ${registryRepo()}@${registryRef()}.`,
121
+ );
122
+ }
123
+ return commitSha;
124
+ }
125
+
126
+ async function fetchRegistryTree(commitSha) {
127
+ const url = `https://api.github.com/repos/${encodedRegistryRepoPath()}/git/trees/${commitSha}?recursive=1`;
128
+ const payload = JSON.parse(await fetchRegistry(url, { maxBytes: MAX_REGISTRY_API_BYTES }));
129
+ if (!Array.isArray(payload?.tree)) {
130
+ throw usageError(`Notis app registry returned no file tree for ${registryRepo()}@${commitSha}.`);
131
+ }
132
+ if (payload.truncated === true) {
133
+ throw usageError(
134
+ `Notis app registry tree was truncated for ${registryRepo()}@${commitSha}; refusing an incomplete scaffold catalog.`,
135
+ );
136
+ }
137
+ return payload.tree
138
+ .filter((entry) => entry?.type === 'blob' && typeof entry.path === 'string')
139
+ .map((entry) => ({ path: entry.path, size: Number(entry.size) }));
140
+ }
141
+
142
+ function fetchRegistryFile(path, commitSha, options) {
143
+ const encodedPath = String(path).split('/').map((part) => encodeURIComponent(part)).join('/');
144
+ return fetchRegistry(
145
+ `https://raw.githubusercontent.com/${encodedRegistryRepoPath()}/${commitSha}/${encodedPath}`,
146
+ options,
147
+ );
148
+ }
149
+
150
+ async function mapWithConcurrency(values, concurrency, mapper) {
151
+ const results = new Array(values.length);
152
+ let cursor = 0;
153
+ async function worker() {
154
+ while (cursor < values.length) {
155
+ const index = cursor;
156
+ cursor += 1;
157
+ results[index] = await mapper(values[index], index);
158
+ }
159
+ }
160
+ await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, worker));
161
+ return results;
162
+ }
163
+
164
+ function assertSafeScaffoldSlug(value) {
165
+ const slug = String(value || '');
166
+ if (!slug || slug === '.' || slug === '..' || /[\\/\0]/.test(slug)) {
167
+ throw usageError(`Unsafe scaffold slug: ${value}`);
168
+ }
169
+ return slug;
170
+ }
171
+
172
+ /**
173
+ * Resolve a registry-owned relative path without allowing either slash style,
174
+ * drive-letter paths, or dot segments to escape the temporary scaffold root.
175
+ * Exported so the platform-independent boundary contract can be unit tested on
176
+ * every host, including macOS CI for the Windows backslash case.
177
+ */
178
+ export function resolveScaffoldTargetPath(tempRoot, registryRelativePath) {
179
+ const raw = String(registryRelativePath || '');
180
+ const normalized = raw.replace(/\\/g, '/');
181
+ const parts = normalized.split('/');
182
+ if (
183
+ !normalized
184
+ || normalized.includes('\0')
185
+ || normalized.startsWith('/')
186
+ || /^[A-Za-z]:\//.test(normalized)
187
+ || parts.some((part) => !part || part === '.' || part === '..')
188
+ ) {
189
+ throw usageError(`Refusing unsafe path from Notis app registry: ${registryRelativePath}`);
190
+ }
191
+
192
+ const root = resolve(tempRoot);
193
+ const target = resolve(root, ...parts);
194
+ const fromRoot = relative(root, target);
195
+ if (!fromRoot || fromRoot === '..' || fromRoot.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || isAbsolute(fromRoot)) {
196
+ throw usageError(`Refusing path outside scaffold directory: ${registryRelativePath}`);
197
+ }
198
+ return target;
199
+ }
200
+
201
+ function readTsStringProperty(source, propertyName) {
202
+ const match = source.match(new RegExp(`\\b${propertyName}\\s*:\\s*(['"\`])([\\s\\S]*?)\\1`));
203
+ return match ? match[2] : null;
204
+ }
205
+
206
+ function catalogEntry(slug, listing, configSource) {
207
+ const description = listing?.description || readTsStringProperty(configSource, 'description') || '';
208
+ return {
209
+ slug,
210
+ name: listing?.name || readTsStringProperty(configSource, 'title') || slug,
211
+ description,
212
+ icon: readTsStringProperty(configSource, 'icon') || 'phosphor:squares-four',
213
+ categories: Array.isArray(listing?.categories) ? listing.categories.filter(Boolean) : [],
214
+ tagline: listing?.tagline || readTsStringProperty(configSource, 'tagline') || description,
215
+ };
216
+ }
217
+
218
+ function readOptionalFile(path) {
219
+ return existsSync(path) ? readFileSync(path, 'utf-8') : '';
220
+ }
221
+
222
+ function loadLocalCatalog(registryDir) {
223
+ const appsDir = join(registryDir, 'apps');
224
+ if (!existsSync(appsDir)) {
225
+ return [];
226
+ }
227
+ const scaffolds = [];
228
+ for (const entry of readdirSync(appsDir, { withFileTypes: true })) {
229
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_')) {
230
+ continue;
231
+ }
232
+ const appDir = join(appsDir, entry.name);
233
+ if (!existsSync(join(appDir, 'notis.config.ts'))) {
234
+ continue;
235
+ }
236
+ const listingPath = join(appDir, 'notis-listing.json');
237
+ const listing = existsSync(listingPath) ? JSON.parse(readFileSync(listingPath, 'utf-8')) : null;
238
+ scaffolds.push(catalogEntry(entry.name, listing, readOptionalFile(join(appDir, 'notis.config.ts'))));
239
+ }
240
+ return scaffolds.sort((a, b) => a.slug.localeCompare(b.slug));
241
+ }
242
+
243
+ /**
244
+ * List every published Store app as a scaffold.
245
+ */
246
+ export async function loadScaffoldCatalog() {
247
+ const registryDir = localRegistryDir();
248
+ if (registryDir) {
249
+ return loadLocalCatalog(registryDir);
250
+ }
251
+ const commitSha = await resolveRegistryCommit();
252
+ const tree = (await fetchRegistryTree(commitSha)).map((entry) => entry.path);
253
+ const slugs = [...new Set(
254
+ tree
255
+ .filter((path) => /^apps\/[^/]+\//.test(path))
256
+ .map((path) => path.split('/')[1]),
257
+ )].filter((slug) => tree.includes(`apps/${slug}/notis.config.ts`));
258
+ if (slugs.length > MAX_CATALOG_ENTRIES) {
259
+ throw usageError(`Notis app registry contains too many scaffold entries (${slugs.length}; maximum ${MAX_CATALOG_ENTRIES}).`);
260
+ }
261
+ let catalogBytes = 0;
262
+ const scaffolds = await mapWithConcurrency(slugs, DOWNLOAD_CONCURRENCY, async (slug) => {
263
+ const [listingText, configSource] = await Promise.all([
264
+ tree.includes(`apps/${slug}/notis-listing.json`)
265
+ ? fetchRegistryFile(`apps/${slug}/notis-listing.json`, commitSha, { maxBytes: MAX_CATALOG_METADATA_BYTES })
266
+ : Promise.resolve(''),
267
+ fetchRegistryFile(`apps/${slug}/notis.config.ts`, commitSha, { maxBytes: MAX_CATALOG_METADATA_BYTES }),
268
+ ]);
269
+ catalogBytes += Buffer.byteLength(listingText) + Buffer.byteLength(configSource);
270
+ if (catalogBytes > MAX_CATALOG_TOTAL_BYTES) {
271
+ throw usageError(
272
+ `Notis app scaffold catalog exceeds the ${MAX_CATALOG_TOTAL_BYTES}-byte aggregate metadata limit.`,
273
+ );
274
+ }
275
+ const listing = listingText ? JSON.parse(listingText) : null;
276
+ return catalogEntry(slug, listing, configSource);
277
+ });
278
+ return scaffolds.sort((a, b) => a.slug.localeCompare(b.slug));
279
+ }
280
+
281
+ /**
282
+ * Case-insensitive match over slug, name, tagline, description, and categories.
283
+ */
284
+ export function filterScaffoldCatalog(catalog, searchTerm) {
285
+ const term = String(searchTerm || '').trim().toLowerCase();
286
+ if (!term) {
287
+ return catalog;
288
+ }
289
+ const words = term.split(/\s+/);
290
+ return catalog.filter((entry) => {
291
+ const haystack = [entry.slug, entry.name, entry.tagline, entry.description, ...(entry.categories || [])]
292
+ .join(' ')
293
+ .toLowerCase();
294
+ return words.every((word) => haystack.includes(word));
295
+ });
296
+ }
297
+
298
+ /**
299
+ * Materialize one published app's source for `apps init --from`.
300
+ *
301
+ * Returns `{ dir, cleanup }` or null when the slug is not in the registry.
302
+ * The caller copies out of `dir` and must call `cleanup()` afterwards.
303
+ */
304
+ export async function acquireScaffoldSource(fromSlug) {
305
+ const safeSlug = assertSafeScaffoldSlug(fromSlug);
306
+ const registryDir = localRegistryDir();
307
+ if (registryDir) {
308
+ const appDir = join(registryDir, 'apps', safeSlug);
309
+ if (!existsSync(join(appDir, 'notis.config.ts'))) {
310
+ return null;
311
+ }
312
+ return { dir: appDir, cleanup: () => {} };
313
+ }
314
+
315
+ const commitSha = await resolveRegistryCommit();
316
+ const tree = await fetchRegistryTree(commitSha);
317
+ const prefix = `apps/${safeSlug}/`;
318
+ const files = tree
319
+ .filter((entry) => entry.path.startsWith(prefix))
320
+ .map((entry) => ({ ...entry, rel: entry.path.slice(prefix.length) }))
321
+ .filter((entry) => entry.rel && !REGISTRY_ARTIFACT.test(entry.rel) && !SKIPPED_SOURCE.test(entry.rel));
322
+ if (!files.some((entry) => entry.rel === 'notis.config.ts')) {
323
+ return null;
324
+ }
325
+ if (files.length > MAX_SCAFFOLD_FILES) {
326
+ throw usageError(`Published scaffold contains too many files (${files.length}; maximum ${MAX_SCAFFOLD_FILES}).`);
327
+ }
328
+ const declaredBytes = files.reduce((total, entry) => (
329
+ Number.isFinite(entry.size) && entry.size >= 0 ? total + entry.size : total
330
+ ), 0);
331
+ if (files.some((entry) => Number.isFinite(entry.size) && entry.size > MAX_SCAFFOLD_FILE_BYTES)) {
332
+ throw usageError(`Published scaffold contains a file larger than ${MAX_SCAFFOLD_FILE_BYTES} bytes.`);
333
+ }
334
+ if (declaredBytes > MAX_SCAFFOLD_TOTAL_BYTES) {
335
+ throw usageError(`Published scaffold source exceeds ${MAX_SCAFFOLD_TOTAL_BYTES} bytes.`);
336
+ }
337
+
338
+ const tempRoot = mkdtempSync(join(tmpdir(), 'notis-scaffold-'));
339
+ let downloadedBytes = 0;
340
+ try {
341
+ for (let index = 0; index < files.length; index += DOWNLOAD_CONCURRENCY) {
342
+ const settled = await Promise.allSettled(
343
+ files.slice(index, index + DOWNLOAD_CONCURRENCY).map(async (entry) => {
344
+ const target = resolveScaffoldTargetPath(tempRoot, entry.rel);
345
+ const data = await fetchRegistryFile(prefix + entry.rel, commitSha, {
346
+ binary: true,
347
+ maxBytes: MAX_SCAFFOLD_FILE_BYTES,
348
+ });
349
+ downloadedBytes += data.length;
350
+ if (downloadedBytes > MAX_SCAFFOLD_TOTAL_BYTES) {
351
+ throw usageError(`Published scaffold source exceeds ${MAX_SCAFFOLD_TOTAL_BYTES} bytes.`);
352
+ }
353
+ mkdirSync(dirname(target), { recursive: true });
354
+ writeFileSync(target, data);
355
+ }),
356
+ );
357
+ const failed = settled.find((result) => result.status === 'rejected');
358
+ if (failed) {
359
+ throw failed.reason;
360
+ }
361
+ }
362
+ } catch (error) {
363
+ rmSync(tempRoot, { recursive: true, force: true });
364
+ throw error;
365
+ }
366
+ return { dir: tempRoot, cleanup: () => rmSync(tempRoot, { recursive: true, force: true }) };
367
+ }
@@ -0,0 +1,20 @@
1
+ export interface BaseSkillReconcileResult {
2
+ installed: number;
3
+ linked: number;
4
+ unchanged: number;
5
+ backups: string[];
6
+ skills: string[];
7
+ }
8
+
9
+ export const BASE_SKILL_NAMES: readonly string[];
10
+
11
+ export function resolveBundledBaseSkillsRoot(options?: {
12
+ resourcesPath?: string;
13
+ }): string;
14
+
15
+ export function reconcileBaseSkills(options?: {
16
+ home?: string;
17
+ userId?: string | null;
18
+ sourceRoot?: string;
19
+ now?: number;
20
+ }): BaseSkillReconcileResult;
@@ -0,0 +1,167 @@
1
+ import {
2
+ cpSync,
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readlinkSync,
7
+ renameSync,
8
+ rmSync,
9
+ symlinkSync,
10
+ } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { dirname, join, relative, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ export const BASE_SKILL_NAMES = Object.freeze([
16
+ 'notis-apps',
17
+ 'notis-query',
18
+ 'notis-cli',
19
+ ]);
20
+
21
+ const HERE = dirname(fileURLToPath(import.meta.url));
22
+
23
+ export function resolveBundledBaseSkillsRoot({ resourcesPath = process.resourcesPath } = {}) {
24
+ const sourceCheckout = resolve(HERE, '../../../../server/skills');
25
+ const packaged = resolve(HERE, '../../dist/base-skills');
26
+ const desktopResource = resourcesPath ? join(resourcesPath, 'base-skills') : null;
27
+ // Electron Packager copies each extraResource directory under its basename,
28
+ // so the three skill folders sit directly in process.resourcesPath.
29
+ const desktopPackagerRoot = resourcesPath || null;
30
+ for (const candidate of [sourceCheckout, packaged, desktopResource, desktopPackagerRoot]) {
31
+ if (
32
+ candidate
33
+ && BASE_SKILL_NAMES.every((name) => existsSync(join(candidate, name, 'SKILL.md')))
34
+ ) {
35
+ return candidate;
36
+ }
37
+ }
38
+ return packaged;
39
+ }
40
+
41
+ export function getBaseSkillPaths({ home = homedir(), userId = null } = {}) {
42
+ const notisRoot = join(home, '.notis', 'skills');
43
+ const targetRoots = [
44
+ join(home, '.agents', 'skills'),
45
+ join(home, '.codex', 'skills'),
46
+ join(home, '.cursor', 'skills'),
47
+ join(home, '.claude', 'skills'),
48
+ ];
49
+ if (userId) {
50
+ targetRoots.push(join(notisRoot, 'users', userId, 'skills'));
51
+ }
52
+ return {
53
+ baseRoot: join(notisRoot, 'base'),
54
+ backupRoot: join(notisRoot, 'backups'),
55
+ targetRoots,
56
+ };
57
+ }
58
+
59
+ function sameLinkTarget(linkPath, expectedTarget) {
60
+ try {
61
+ if (!lstatSync(linkPath).isSymbolicLink()) return false;
62
+ return resolve(dirname(linkPath), readlinkSync(linkPath)) === resolve(expectedTarget);
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ function backupConflict(linkPath, backupRoot, targetLabel, skillName, now) {
69
+ const backupPath = join(backupRoot, String(now), targetLabel, skillName);
70
+ mkdirSync(dirname(backupPath), { recursive: true, mode: 0o700 });
71
+ renameSync(linkPath, backupPath);
72
+ return backupPath;
73
+ }
74
+
75
+ function replaceDirectory(source, destination) {
76
+ mkdirSync(dirname(destination), { recursive: true, mode: 0o700 });
77
+ const temporary = `${destination}.tmp-${process.pid}-${Date.now()}`;
78
+ const previous = `${destination}.previous-${process.pid}-${Date.now()}`;
79
+ rmSync(temporary, { recursive: true, force: true });
80
+ cpSync(source, temporary, { recursive: true });
81
+ let movedPrevious = false;
82
+ try {
83
+ if (existsSync(destination)) {
84
+ renameSync(destination, previous);
85
+ movedPrevious = true;
86
+ }
87
+ renameSync(temporary, destination);
88
+ if (movedPrevious) rmSync(previous, { recursive: true, force: true });
89
+ } catch (error) {
90
+ rmSync(temporary, { recursive: true, force: true });
91
+ if (movedPrevious && !existsSync(destination) && existsSync(previous)) {
92
+ renameSync(previous, destination);
93
+ }
94
+ throw error;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Install the three CLI-owned system skills and expose them to every supported
100
+ * local agent. This intentionally does not consult the account sync setting:
101
+ * that setting controls only Electron's repeating account-skill refresh.
102
+ */
103
+ export function reconcileBaseSkills({
104
+ home = homedir(),
105
+ userId = null,
106
+ sourceRoot = resolveBundledBaseSkillsRoot(),
107
+ now = Date.now(),
108
+ } = {}) {
109
+ const paths = getBaseSkillPaths({ home, userId });
110
+ const result = {
111
+ installed: 0,
112
+ linked: 0,
113
+ unchanged: 0,
114
+ backups: [],
115
+ skills: [...BASE_SKILL_NAMES],
116
+ };
117
+
118
+ for (const name of BASE_SKILL_NAMES) {
119
+ const source = join(sourceRoot, name);
120
+ if (!existsSync(join(source, 'SKILL.md'))) {
121
+ throw new Error(`Bundled base skill is missing: ${name}`);
122
+ }
123
+ replaceDirectory(source, join(paths.baseRoot, name));
124
+ result.installed += 1;
125
+ }
126
+
127
+ for (const targetRoot of paths.targetRoots) {
128
+ mkdirSync(targetRoot, { recursive: true, mode: 0o700 });
129
+ const targetLabel = relative(home, targetRoot).replaceAll('/', '_') || 'home';
130
+ for (const name of BASE_SKILL_NAMES) {
131
+ const skillTarget = join(paths.baseRoot, name);
132
+ const linkPath = join(targetRoot, name);
133
+ if (sameLinkTarget(linkPath, skillTarget)) {
134
+ result.unchanged += 1;
135
+ continue;
136
+ }
137
+ if (existsSync(linkPath) || (() => {
138
+ try { lstatSync(linkPath); return true; } catch { return false; }
139
+ })()) {
140
+ // sameLinkTarget already accepted links owned by this installer. Every
141
+ // other path, including a user-created symlink, is a conflict whose
142
+ // identity and target must remain recoverable.
143
+ result.backups.push(backupConflict(linkPath, paths.backupRoot, targetLabel, name, now));
144
+ }
145
+ const relativeTarget = relative(targetRoot, skillTarget) || '.';
146
+ symlinkSync(
147
+ process.platform === 'win32' ? skillTarget : relativeTarget,
148
+ linkPath,
149
+ process.platform === 'win32' ? 'junction' : undefined,
150
+ );
151
+ result.linked += 1;
152
+ }
153
+ }
154
+
155
+ return result;
156
+ }
157
+
158
+ export function reconcileBaseSkillsBestEffort(options = {}) {
159
+ try {
160
+ return reconcileBaseSkills(options);
161
+ } catch (error) {
162
+ if (process.env.NOTIS_DEBUG_BASE_SKILLS === '1') {
163
+ process.stderr.write(`[notis] Base skill reconciliation failed: ${error.message}\n`);
164
+ }
165
+ return null;
166
+ }
167
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Detect that this CLI is running *as* a Notis agent rather than for a person.
3
+ *
4
+ * Hand-over is the one command where that distinction matters: an agent Notis
5
+ * is already running has no business handing the same work back to Notis, and
6
+ * nothing about the loop it would create is self-limiting.
7
+ *
8
+ * This is the polite half of the guard. The server refuses the same call unless
9
+ * it carries a purpose-scoped CLI/MCP OAuth token, which a delegated runtime
10
+ * never has -- that is the half that actually holds. What this adds is a clear
11
+ * message at the point of the mistake, instead of a permission error the agent
12
+ * will try to work around.
13
+ */
14
+
15
+ import { existsSync } from 'node:fs';
16
+ import { CliError, EXIT_CODES } from './errors.js';
17
+
18
+ // The cloud computer's root. Present only inside the user's Vercel sandbox.
19
+ const SANDBOX_ROOT = '/vercel/sandbox';
20
+
21
+ export function delegatedContextReason(env = process.env, { fileExists = existsSync } = {}) {
22
+ if (env.NOTIS_DELEGATED_CONTEXT === '1') {
23
+ // Set by Notis Desktop on every coding agent it spawns. Those runs use the
24
+ // user's own OAuth profile, so nothing else distinguishes them.
25
+ return 'this process was started by Notis as a delegated coding agent';
26
+ }
27
+ if (fileExists(SANDBOX_ROOT)) {
28
+ return 'this process is running on the Notis cloud computer';
29
+ }
30
+ if (env.NOTIS_AGENT === '1' && env.NOTIS_JWT) {
31
+ return 'this process is authenticated as a Notis agent, not as you';
32
+ }
33
+ return null;
34
+ }
35
+
36
+ export function assertNotDelegated(commandLabel, env = process.env) {
37
+ const reason = delegatedContextReason(env);
38
+ if (!reason) {
39
+ return;
40
+ }
41
+ throw new CliError({
42
+ code: 'handover_from_delegated_context',
43
+ message:
44
+ `\`notis ${commandLabel}\` hands work to a Notis agent, and ${reason}. ` +
45
+ 'An agent cannot hand its own task back to Notis.',
46
+ exitCode: EXIT_CODES.usage,
47
+ hints: [
48
+ { message: 'Run the hand-over from the terminal on your own machine.' },
49
+ { message: 'If you are the agent: just do the work here, in this workspace.' },
50
+ ],
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Tools that hand work to a Notis agent, by canonical name.
56
+ *
57
+ * `notis handover` is not the only way to reach these: `notis tools exec` takes
58
+ * any tool name, and `tools exec-parallel` takes a list of them. Guarding only
59
+ * the friendly command would leave the escape hatch it exists to wrap.
60
+ */
61
+ export const HANDOVER_TOOL_NAMES = new Set(['LOCAL_NOTIS_HAND_OVER']);
62
+
63
+ export function assertToolNotDelegated(toolName, env = process.env) {
64
+ if (!HANDOVER_TOOL_NAMES.has(String(toolName || '').toUpperCase())) {
65
+ return;
66
+ }
67
+ assertNotDelegated(`tools exec ${toolName}`, env);
68
+ }