@notis_ai/cli 0.2.18 → 0.2.20

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 (395) hide show
  1. package/README.md +542 -146
  2. package/bin/check-runtime.js +15 -0
  3. package/bin/notis.js +2 -0
  4. package/config/notis_app_design_rules.json +320 -0
  5. package/config/portal-route-roots.json +45 -0
  6. package/dist/agent-hooks/notis-agent-hook.mjs +42325 -18273
  7. package/dist/base-skills/notis-cli/.notis-base-skill.json +1 -0
  8. package/dist/base-skills/notis-cli/SKILL.md +51 -280
  9. package/dist/base-skills/notis-cli/references/app-delivery.md +17 -0
  10. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  11. package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
  12. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  13. package/dist/base-skills/notis-cli/references/troubleshooting.md +67 -0
  14. package/dist/base-skills/notis-query/.notis-base-skill.json +1 -0
  15. package/dist/base-skills/notis-query/SKILL.md +35 -699
  16. package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
  17. package/dist/base-skills/notis-query/references/documents.md +74 -0
  18. package/dist/base-skills/notis-query/references/query.md +76 -0
  19. package/dist/base-skills/notis-spaces/.notis-base-skill.json +1 -0
  20. package/dist/base-skills/notis-spaces/SKILL.md +218 -0
  21. package/dist/base-skills/notis-spaces/references/architecture.md +116 -0
  22. package/dist/base-skills/notis-spaces/references/context.md +81 -0
  23. package/dist/base-skills/notis-spaces/references/design.md +141 -0
  24. package/dist/base-skills/notis-spaces/references/fixtures.md +29 -0
  25. package/dist/base-skills/notis-spaces/references/memory.md +58 -0
  26. package/dist/base-skills/notis-spaces/references/reading.md +73 -0
  27. package/dist/base-skills/notis-spaces/references/release.md +120 -0
  28. package/dist/base-skills/notis-spaces/references/reports.md +77 -0
  29. package/dist/base-skills/notis-spaces/references/sdk.md +273 -0
  30. package/dist/base-skills/notis-spaces/references/store.md +118 -0
  31. package/dist/base-skills/notis-spaces/references/troubleshooting.md +28 -0
  32. package/dist/base-skills/notis-spaces/references/views.md +229 -0
  33. package/{template/packages → dist}/sdk/package.json +9 -10
  34. package/dist/sdk/src/agentContext.ts +36 -0
  35. package/dist/sdk/src/bundleModule.ts +175 -0
  36. package/dist/sdk/src/components/Dialog.tsx +103 -0
  37. package/dist/sdk/src/components/DocumentEditor.tsx +113 -0
  38. package/dist/sdk/src/components/DocumentPage.tsx +184 -0
  39. package/{template/packages → dist}/sdk/src/components/Markdown.tsx +9 -3
  40. package/{template/packages → dist}/sdk/src/components/MarkdownEditor.tsx +26 -5
  41. package/{template/packages → dist}/sdk/src/components/MultiSelectActionBar.tsx +95 -12
  42. package/{template/packages → dist}/sdk/src/components/MultiSelectCheckbox.tsx +4 -2
  43. package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  44. package/{template/packages → dist}/sdk/src/components/NotisSelectionBoundary.tsx +9 -5
  45. package/dist/sdk/src/components/RecordComponents.tsx +111 -0
  46. package/dist/sdk/src/components/Skeleton.tsx +38 -0
  47. package/dist/sdk/src/config.ts +31 -0
  48. package/{template/packages → dist}/sdk/src/documents.ts +21 -6
  49. package/dist/sdk/src/hooks/useAction.ts +28 -0
  50. package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
  51. package/{template/packages → dist}/sdk/src/hooks/useBackend.ts +3 -2
  52. package/dist/sdk/src/hooks/useCloudComputer.ts +94 -0
  53. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  54. package/{template/packages → dist}/sdk/src/hooks/useCollectionInteractions.ts +157 -147
  55. package/dist/sdk/src/hooks/useDatabaseSchema.ts +50 -0
  56. package/{template/packages → dist}/sdk/src/hooks/useDatabaseSubscription.ts +2 -2
  57. package/dist/sdk/src/hooks/useDocument.ts +51 -0
  58. package/{template/packages → dist}/sdk/src/hooks/useDocuments.ts +25 -59
  59. package/{template/packages → dist}/sdk/src/hooks/useHandover.ts +7 -7
  60. package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
  61. package/dist/sdk/src/hooks/useMarqueeSelection.ts +194 -0
  62. package/{template/packages → dist}/sdk/src/hooks/useMultiSelect.ts +2 -8
  63. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  64. package/dist/sdk/src/hooks/useNativeDocuments.ts +143 -0
  65. package/{template/packages → dist}/sdk/src/hooks/useNotis.ts +6 -1
  66. package/dist/sdk/src/hooks/useNotisNavigation.ts +70 -0
  67. package/dist/sdk/src/hooks/useQuery.ts +81 -0
  68. package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
  69. package/dist/sdk/src/hooks/useTopBarSearch.ts +102 -0
  70. package/dist/sdk/src/hooks/useTopBarTitleLeaf.ts +75 -0
  71. package/{template/packages → dist}/sdk/src/hooks/useUpsertDocument.ts +1 -1
  72. package/dist/sdk/src/hooks/useViewParams.ts +195 -0
  73. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  74. package/{template/packages → dist}/sdk/src/index.ts +64 -1
  75. package/{template/packages → dist}/sdk/src/interactions/actions.ts +14 -1
  76. package/{template/packages → dist}/sdk/src/interactions/shortcuts.tsx +129 -23
  77. package/dist/sdk/src/interactions/visibility.ts +13 -0
  78. package/{template/packages → dist}/sdk/src/interactions.ts +5 -1
  79. package/dist/sdk/src/nativeData.ts +39 -0
  80. package/{template/packages → dist}/sdk/src/provider.tsx +9 -4
  81. package/dist/sdk/src/queryCache.ts +292 -0
  82. package/dist/sdk/src/queryErrors.ts +51 -0
  83. package/dist/sdk/src/runtime.ts +675 -0
  84. package/dist/sdk/src/runtimeModules.ts +33 -0
  85. package/dist/sdk/src/space.ts +442 -0
  86. package/dist/sdk/src/standardViewParams.ts +36 -0
  87. package/{template/packages → dist}/sdk/src/styles.css +81 -16
  88. package/dist/sdk/src/tailwind.ts +66 -0
  89. package/dist/sdk/src/ui/charts/RenderChartData.tsx +25 -0
  90. package/dist/sdk/src/ui/engine/core.ts +392 -0
  91. package/dist/sdk/src/ui/engine/dom.ts +183 -0
  92. package/dist/sdk/src/ui/engine/hooks.ts +195 -0
  93. package/dist/sdk/src/ui/engine/instance.ts +63 -0
  94. package/dist/sdk/src/ui/engine/keycapContext.ts +2 -0
  95. package/dist/sdk/src/ui/engine/registry.ts +262 -0
  96. package/dist/sdk/src/ui/engine/types.ts +62 -0
  97. package/dist/sdk/src/ui/icons.tsx +91 -0
  98. package/dist/sdk/src/ui/util.ts +83 -0
  99. package/dist/sdk/src/viewPeriod.ts +26 -0
  100. package/dist/sdk/src/vite.ts +92 -0
  101. package/dist/sdk/src/words/actions.ts +295 -0
  102. package/dist/sdk/src/words/clashes.ts +158 -0
  103. package/dist/sdk/src/words/format.ts +136 -0
  104. package/dist/sdk/src/words/index.ts +13 -0
  105. package/dist/sdk/src/words/keymap.ts +72 -0
  106. package/dist/sdk/src/words/keys.ts +297 -0
  107. package/dist/sdk/src/words/shell.ts +46 -0
  108. package/dist/sdk/src/words/types.ts +65 -0
  109. package/dist/skill-sync/index.js +832 -151
  110. package/dist/skill-sync/index.js.map +4 -4
  111. package/dist/skill-sync-worker.mjs +3941 -0
  112. package/dist/space-harness/host.js +1460 -0
  113. package/dist/space-sdk/dist/chunk-ZSN57ATH.js +5 -0
  114. package/dist/space-sdk/dist/index.js +15 -0
  115. package/dist/space-sdk/dist/ui.js +18 -0
  116. package/dist/space-sdk/package.json +33 -0
  117. package/dist/space-sdk/src/agentContext.d.ts +36 -0
  118. package/dist/space-sdk/src/components/Dialog.d.ts +20 -0
  119. package/dist/space-sdk/src/components/DocumentEditor.d.ts +46 -0
  120. package/dist/space-sdk/src/components/DocumentPage.d.ts +53 -0
  121. package/dist/space-sdk/src/components/Markdown.d.ts +18 -0
  122. package/dist/space-sdk/src/components/MarkdownEditor.d.ts +11 -0
  123. package/dist/space-sdk/src/components/MultiSelectActionBar.d.ts +31 -0
  124. package/dist/space-sdk/src/components/MultiSelectCheckbox.d.ts +14 -0
  125. package/dist/space-sdk/src/components/MultiSelectDragOverlay.d.ts +8 -0
  126. package/dist/space-sdk/src/components/NotisCommentBoundary.d.ts +36 -0
  127. package/dist/space-sdk/src/components/NotisSelectionBoundary.d.ts +16 -0
  128. package/dist/space-sdk/src/components/RecordComponents.d.ts +64 -0
  129. package/dist/space-sdk/src/components/ShortcutHints.d.ts +12 -0
  130. package/dist/space-sdk/src/components/Skeleton.d.ts +23 -0
  131. package/dist/space-sdk/src/config.ts +31 -0
  132. package/dist/space-sdk/src/documents.d.ts +59 -0
  133. package/dist/space-sdk/src/hooks/useAction.d.ts +8 -0
  134. package/dist/space-sdk/src/hooks/useActiveResource.d.ts +3 -0
  135. package/dist/space-sdk/src/hooks/useAgentContext.d.ts +7 -0
  136. package/dist/space-sdk/src/hooks/useBackend.d.ts +23 -0
  137. package/dist/space-sdk/src/hooks/useCloudComputer.d.ts +45 -0
  138. package/dist/space-sdk/src/hooks/useCloudComputerShell.d.ts +49 -0
  139. package/dist/space-sdk/src/hooks/useCollectionInteractions.d.ts +118 -0
  140. package/dist/space-sdk/src/hooks/useDatabaseSchema.d.ts +14 -0
  141. package/dist/space-sdk/src/hooks/useDatabaseSubscription.d.ts +26 -0
  142. package/dist/space-sdk/src/hooks/useDocument.d.ts +22 -0
  143. package/dist/space-sdk/src/hooks/useDocuments.d.ts +31 -0
  144. package/dist/space-sdk/src/hooks/useHandover.d.ts +43 -0
  145. package/dist/space-sdk/src/hooks/useLongPressSelection.d.ts +9 -0
  146. package/dist/space-sdk/src/hooks/useMarqueeSelection.d.ts +57 -0
  147. package/dist/space-sdk/src/hooks/useMultiSelect.d.ts +51 -0
  148. package/dist/space-sdk/src/hooks/useNativeDocumentBody.d.ts +24 -0
  149. package/dist/space-sdk/src/hooks/useNativeDocuments.d.ts +55 -0
  150. package/dist/space-sdk/src/hooks/useNotis.d.ts +25 -0
  151. package/dist/space-sdk/src/hooks/useNotisNavigation.d.ts +33 -0
  152. package/dist/space-sdk/src/hooks/useQuery.d.ts +22 -0
  153. package/dist/space-sdk/src/hooks/useTool.d.ts +20 -0
  154. package/dist/space-sdk/src/hooks/useToolQuery.d.ts +3 -0
  155. package/dist/space-sdk/src/hooks/useTools.d.ts +15 -0
  156. package/dist/space-sdk/src/hooks/useTopBarSearch.d.ts +48 -0
  157. package/dist/space-sdk/src/hooks/useTopBarTitleLeaf.d.ts +24 -0
  158. package/dist/space-sdk/src/hooks/useUpsertDocument.d.ts +27 -0
  159. package/dist/space-sdk/src/hooks/useViewParams.d.ts +76 -0
  160. package/dist/space-sdk/src/hooks/useViewerReads.d.ts +38 -0
  161. package/dist/space-sdk/src/index.d.ts +98 -0
  162. package/dist/space-sdk/src/interactions/actions.d.ts +33 -0
  163. package/dist/space-sdk/src/interactions/shortcuts.d.ts +42 -0
  164. package/dist/space-sdk/src/interactions/visibility.d.ts +2 -0
  165. package/dist/space-sdk/src/nativeData.d.ts +66 -0
  166. package/dist/space-sdk/src/provider.d.ts +17 -0
  167. package/dist/space-sdk/src/queryCache.d.ts +45 -0
  168. package/dist/space-sdk/src/queryErrors.ts +51 -0
  169. package/dist/space-sdk/src/runtime.d.ts +615 -0
  170. package/dist/space-sdk/src/runtimeModules.ts +33 -0
  171. package/dist/space-sdk/src/space.ts +442 -0
  172. package/dist/space-sdk/src/standardViewParams.d.ts +15 -0
  173. package/dist/space-sdk/src/tailwind-candidates.d.ts +3418 -0
  174. package/dist/space-sdk/src/tailwind.ts +66 -0
  175. package/dist/space-sdk/src/ui/actions/ActionToast.d.ts +15 -0
  176. package/dist/space-sdk/src/ui/actions/actionWords.d.ts +16 -0
  177. package/dist/space-sdk/src/ui/actions/index.d.ts +7 -0
  178. package/dist/space-sdk/src/ui/actions/useActions.d.ts +69 -0
  179. package/dist/space-sdk/src/ui/actions/useToast.d.ts +41 -0
  180. package/dist/space-sdk/src/ui/blocks/Tabs.d.ts +139 -0
  181. package/dist/space-sdk/src/ui/blocks/blocks.d.ts +217 -0
  182. package/dist/space-sdk/src/ui/blocks/index.d.ts +4 -0
  183. package/dist/space-sdk/src/ui/board/BoardView.d.ts +36 -0
  184. package/dist/space-sdk/src/ui/board/boardModel.d.ts +46 -0
  185. package/dist/space-sdk/src/ui/board/boardWords.d.ts +9 -0
  186. package/dist/space-sdk/src/ui/board/index.d.ts +5 -0
  187. package/dist/space-sdk/src/ui/calendar/CalendarView.d.ts +49 -0
  188. package/dist/space-sdk/src/ui/calendar/DayPicker.d.ts +23 -0
  189. package/dist/space-sdk/src/ui/calendar/calendarMath.d.ts +81 -0
  190. package/dist/space-sdk/src/ui/charts/Chart.d.ts +203 -0
  191. package/dist/space-sdk/src/ui/charts/ChartTable.d.ts +37 -0
  192. package/dist/space-sdk/src/ui/charts/RenderChartData.d.ts +16 -0
  193. package/dist/space-sdk/src/ui/charts/StatRow.d.ts +59 -0
  194. package/dist/space-sdk/src/ui/charts/chartData.d.ts +84 -0
  195. package/dist/space-sdk/src/ui/comments/CommentPane.d.ts +58 -0
  196. package/dist/space-sdk/src/ui/comments/commentModel.d.ts +157 -0
  197. package/dist/space-sdk/src/ui/comments/commentWords.d.ts +60 -0
  198. package/dist/space-sdk/src/ui/comments/index.d.ts +4 -0
  199. package/dist/space-sdk/src/ui/comments/useComments.d.ts +146 -0
  200. package/dist/space-sdk/src/ui/engine/HelpSheet.d.ts +12 -0
  201. package/dist/space-sdk/src/ui/engine/KeyboardEngine.d.ts +27 -0
  202. package/dist/space-sdk/src/ui/engine/core.d.ts +73 -0
  203. package/dist/space-sdk/src/ui/engine/hooks.d.ts +64 -0
  204. package/dist/space-sdk/src/ui/engine/index.d.ts +14 -0
  205. package/dist/space-sdk/src/ui/engine/instance.d.ts +21 -0
  206. package/dist/space-sdk/src/ui/engine/keycapContext.d.ts +2 -0
  207. package/dist/space-sdk/src/ui/engine/registry.d.ts +77 -0
  208. package/dist/space-sdk/src/ui/engine/step.d.ts +11 -0
  209. package/dist/space-sdk/src/ui/engine/types.d.ts +54 -0
  210. package/dist/space-sdk/src/ui/fields/fieldWords.d.ts +56 -0
  211. package/dist/space-sdk/src/ui/fields/fields.d.ts +162 -0
  212. package/dist/space-sdk/src/ui/fields/index.d.ts +5 -0
  213. package/dist/space-sdk/src/ui/fields/useFieldDraft.d.ts +22 -0
  214. package/dist/space-sdk/src/ui/gallery/AttachmentGallery.d.ts +53 -0
  215. package/dist/space-sdk/src/ui/gallery/ImageViewer.d.ts +27 -0
  216. package/dist/space-sdk/src/ui/gallery/galleryWords.d.ts +95 -0
  217. package/dist/space-sdk/src/ui/gallery/index.d.ts +5 -0
  218. package/dist/space-sdk/src/ui/icons.d.ts +13 -0
  219. package/dist/space-sdk/src/ui/index.d.ts +54 -0
  220. package/dist/space-sdk/src/ui/keycap/Keycap.d.ts +22 -0
  221. package/dist/space-sdk/src/ui/keycap/index.d.ts +2 -0
  222. package/dist/space-sdk/src/ui/layout/Panels.d.ts +50 -0
  223. package/dist/space-sdk/src/ui/layout/chatPanelEdge.d.ts +77 -0
  224. package/dist/space-sdk/src/ui/layout/index.d.ts +5 -0
  225. package/dist/space-sdk/src/ui/layout/panelMath.d.ts +91 -0
  226. package/dist/space-sdk/src/ui/list/ListReader.d.ts +380 -0
  227. package/dist/space-sdk/src/ui/list/Row.d.ts +145 -0
  228. package/dist/space-sdk/src/ui/list/SelectionBar.d.ts +65 -0
  229. package/dist/space-sdk/src/ui/list/index.d.ts +14 -0
  230. package/dist/space-sdk/src/ui/list/rowState.d.ts +22 -0
  231. package/dist/space-sdk/src/ui/list/useListKeys.d.ts +89 -0
  232. package/dist/space-sdk/src/ui/list/useReorder.d.ts +86 -0
  233. package/dist/space-sdk/src/ui/list/useSelection.d.ts +90 -0
  234. package/dist/space-sdk/src/ui/open/OpenAsMenu.d.ts +15 -0
  235. package/dist/space-sdk/src/ui/open/index.d.ts +6 -0
  236. package/dist/space-sdk/src/ui/open/openModeStore.d.ts +39 -0
  237. package/dist/space-sdk/src/ui/open/useOpenMode.d.ts +17 -0
  238. package/dist/space-sdk/src/ui/overlay/Menu.d.ts +88 -0
  239. package/dist/space-sdk/src/ui/overlay/Popover.d.ts +28 -0
  240. package/dist/space-sdk/src/ui/overlay/Tooltip.d.ts +19 -0
  241. package/dist/space-sdk/src/ui/overlay/index.d.ts +9 -0
  242. package/dist/space-sdk/src/ui/overlay/layerRoot.d.ts +24 -0
  243. package/dist/space-sdk/src/ui/overlay/position.d.ts +31 -0
  244. package/dist/space-sdk/src/ui/period/PeriodPicker.d.ts +30 -0
  245. package/dist/space-sdk/src/ui/period/dateFormat.d.ts +50 -0
  246. package/dist/space-sdk/src/ui/period/dateWords.d.ts +88 -0
  247. package/dist/space-sdk/src/ui/period/periods.d.ts +70 -0
  248. package/dist/space-sdk/src/ui/period/usePeriod.d.ts +23 -0
  249. package/dist/space-sdk/src/ui/present/index.d.ts +3 -0
  250. package/dist/space-sdk/src/ui/present/presentWords.d.ts +27 -0
  251. package/dist/space-sdk/src/ui/present/screens.d.ts +36 -0
  252. package/dist/space-sdk/src/ui/present/useAudienceScreen.d.ts +38 -0
  253. package/dist/space-sdk/src/ui/styles.d.ts +21 -0
  254. package/dist/space-sdk/src/ui/timeline/TimelineView.d.ts +44 -0
  255. package/dist/space-sdk/src/ui/timeline/timelineMath.d.ts +123 -0
  256. package/dist/space-sdk/src/viewPeriod.d.ts +8 -0
  257. package/dist/space-sdk/src/vite.ts +92 -0
  258. package/dist/space-sdk/src/words/actions.ts +295 -0
  259. package/dist/space-sdk/src/words/clashes.ts +158 -0
  260. package/dist/space-sdk/src/words/format.ts +136 -0
  261. package/dist/space-sdk/src/words/index.ts +13 -0
  262. package/dist/space-sdk/src/words/keymap.ts +72 -0
  263. package/dist/space-sdk/src/words/keys.ts +297 -0
  264. package/dist/space-sdk/src/words/shell.ts +46 -0
  265. package/dist/space-sdk/src/words/types.ts +65 -0
  266. package/dist/view-renderer/dist/host.css +1 -0
  267. package/dist/view-renderer/dist/host.js +2374 -0
  268. package/dist/view-renderer/dist/theme.css +73 -0
  269. package/dist/view-renderer/src/dom.js +212 -0
  270. package/dist/view-renderer/src/errors.js +8 -0
  271. package/dist/view-renderer/src/host.tsx +250 -0
  272. package/dist/view-renderer/src/index.js +4 -0
  273. package/dist/view-renderer/src/png.js +213 -0
  274. package/dist/view-renderer/src/pool.js +110 -0
  275. package/dist/view-renderer/src/remote-broker.js +59 -0
  276. package/dist/view-renderer/src/renderer.js +279 -0
  277. package/package.json +21 -11
  278. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +63 -12
  279. package/skills/notis-onboarding/BRIEF.md +14 -9
  280. package/skills/notis-query/cli.md +1 -1
  281. package/skills/notis-spaces/cli.md +540 -0
  282. package/src/cli.js +8 -8
  283. package/src/command-specs/agents.js +121 -8
  284. package/src/command-specs/auth.js +3 -3
  285. package/src/command-specs/diagnostics.js +97 -20
  286. package/src/command-specs/documents.js +158 -0
  287. package/src/command-specs/files.js +29 -0
  288. package/src/command-specs/handover.js +331 -79
  289. package/src/command-specs/helpers.js +50 -40
  290. package/src/command-specs/index.js +14 -5
  291. package/src/command-specs/meta.js +38 -9
  292. package/src/command-specs/onboarding.js +1 -1
  293. package/src/command-specs/profile.js +42 -1
  294. package/src/command-specs/skills.js +314 -9
  295. package/src/command-specs/smoke.js +16 -4
  296. package/src/command-specs/spaces.js +783 -0
  297. package/src/command-specs/tools.js +118 -21
  298. package/src/command-specs/version-history.js +20 -0
  299. package/src/command-specs/views.js +69 -0
  300. package/src/runtime/agent-browser.js +178 -267
  301. package/src/runtime/agent-setup.js +172 -13
  302. package/src/runtime/app-boundary-validator.js +338 -2
  303. package/src/runtime/app-data-literals.js +82 -0
  304. package/src/runtime/app-platform.js +750 -1570
  305. package/src/runtime/auth-recovery.js +17 -0
  306. package/src/runtime/base-skills.d.ts +11 -0
  307. package/src/runtime/base-skills.js +100 -3
  308. package/src/runtime/computer-identity.d.ts +30 -0
  309. package/src/runtime/computer-identity.js +151 -0
  310. package/src/runtime/config-lock.js +135 -0
  311. package/src/runtime/confined-space-build.js +74 -0
  312. package/src/runtime/confined-space-verify.js +49 -0
  313. package/src/runtime/delegated-context.js +5 -21
  314. package/src/runtime/native-record-files.js +47 -0
  315. package/src/runtime/oauth.js +342 -31
  316. package/src/runtime/profiles.js +19 -112
  317. package/src/runtime/retired-sdk-files.js +24 -0
  318. package/src/runtime/retry.js +150 -0
  319. package/src/runtime/skill-sync/cloud-client.ts +33 -8
  320. package/src/runtime/skill-sync/index.ts +443 -106
  321. package/src/runtime/skill-sync/local-scanner.ts +248 -32
  322. package/src/runtime/skill-sync/native-identity.ts +102 -0
  323. package/src/runtime/skill-sync/native-plan.ts +70 -0
  324. package/src/runtime/skill-sync/symlink-manager.ts +66 -16
  325. package/src/runtime/skill-sync/sync-plan.ts +34 -2
  326. package/src/runtime/skill-sync/types.ts +54 -0
  327. package/src/runtime/skill-sync/write-cloud-skill.ts +4 -4
  328. package/src/runtime/skill-sync-service.js +109 -0
  329. package/src/runtime/source-root.js +32 -0
  330. package/src/runtime/space-collection-scaffold.js +107 -0
  331. package/src/runtime/space-database-structures.js +229 -0
  332. package/src/runtime/space-fixture-broker.js +68 -0
  333. package/src/runtime/space-lock.js +337 -0
  334. package/src/runtime/space-platform.js +133 -0
  335. package/src/runtime/space-source.js +290 -0
  336. package/src/runtime/space-test-server.js +354 -0
  337. package/src/runtime/space-verification.js +94 -0
  338. package/src/runtime/space-verify-browser-worker.js +51 -0
  339. package/src/runtime/space-view-manifest.js +368 -0
  340. package/src/runtime/sync-skills.d.ts +5 -1
  341. package/src/runtime/sync-skills.js +11 -2
  342. package/src/runtime/tool-confirmation.js +138 -0
  343. package/src/runtime/transport.js +118 -24
  344. package/src/runtime/upsell.js +219 -0
  345. package/src/runtime/view-render.js +49 -0
  346. package/src/skill-sync-worker-entry.js +2 -0
  347. package/src/skill-sync-worker.js +114 -0
  348. package/src/space-harness-host.ts +122 -0
  349. package/template/.harness/index.html.tmpl +9 -352
  350. package/template/app/globals.css +29 -3
  351. package/template/app/layout.tsx +2 -6
  352. package/template/components/ui/badge.tsx +7 -4
  353. package/template/components/ui/button.tsx +1 -1
  354. package/template/components/ui/native-select.tsx +24 -0
  355. package/template/notis.config.ts +4 -36
  356. package/template/package-lock.json +1800 -1867
  357. package/template/package.json +16 -14
  358. package/template/postcss.config.mjs +1 -1
  359. package/template/tailwind.config.ts +1 -6
  360. package/template/tsconfig.json +1 -0
  361. package/template/vite.config.ts +2 -2
  362. package/dist/base-skills/notis-apps/SKILL.md +0 -613
  363. package/skills/notis-apps/cli.md +0 -285
  364. package/src/command-specs/apps.js +0 -3011
  365. package/src/runtime/app-changelog.js +0 -79
  366. package/src/runtime/app-dev-consumers.js +0 -154
  367. package/src/runtime/app-dev-host-lock.js +0 -80
  368. package/src/runtime/app-dev-process-identity.js +0 -111
  369. package/src/runtime/app-dev-roots.js +0 -284
  370. package/src/runtime/app-dev-server.js +0 -1140
  371. package/src/runtime/app-dev-sessions.js +0 -185
  372. package/src/runtime/app-registry-scaffolds.js +0 -367
  373. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  374. package/src/runtime/cli-mode.generated.js +0 -5
  375. package/src/runtime/cli-mode.js +0 -34
  376. package/src/runtime/store-screenshot.js +0 -143
  377. package/template/CHANGELOG.md +0 -5
  378. package/template/app/page.tsx +0 -60
  379. package/template/components/ui/card.tsx +0 -56
  380. package/template/packages/sdk/src/components/DocumentEditor.tsx +0 -103
  381. package/template/packages/sdk/src/config.ts +0 -259
  382. package/template/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  383. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  384. package/template/packages/sdk/src/hooks/useDocument.ts +0 -78
  385. package/template/packages/sdk/src/hooks/useNotisNavigation.ts +0 -52
  386. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  387. package/template/packages/sdk/src/runtime.ts +0 -439
  388. package/template/packages/sdk/src/vite.ts +0 -56
  389. package/template/packages/sdk/tsconfig.json +0 -15
  390. /package/{template/packages → dist}/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
  391. /package/{template/packages → dist}/sdk/src/components/ShortcutHints.tsx +0 -0
  392. /package/{template/packages → dist}/sdk/src/hooks/useActiveResource.ts +0 -0
  393. /package/{template/packages → dist}/sdk/src/hooks/useTool.ts +0 -0
  394. /package/{template/packages → dist}/sdk/src/hooks/useTools.ts +0 -0
  395. /package/{template/packages → dist}/sdk/src/ui.ts +0 -0
@@ -0,0 +1,368 @@
1
+ /**
2
+ * What a view declares (V4): path, typed params, shows, memory, chrome and a Markdown override.
3
+ * Mirrors server/lib/space_view_manifest.py, which re-checks every release; property-level checks
4
+ * need the linked schemas and run on the server during `spaces verify` and deploy (a record param's
5
+ * page rule is also checked here against a database created in the same release).
6
+ * Every presentation requires path, description and memory, including sources that omit
7
+ * specVersion. Containers remain non-executable; explicit specVersion 2 is unchanged.
8
+ */
9
+ import { usageError } from './errors.js';
10
+ import portalRouteRoots from '../../config/portal-route-roots.json' with { type: 'json' };
11
+ const RESERVED_PATHS = new Set(portalRouteRoots);
12
+
13
+ export const VIEW_SPEC_VERSION = 2;
14
+ export const VIEW_DEFINITION_FIELDS = Object.freeze(['specVersion', 'path', 'params', 'shows', 'memory', 'chrome', 'markdown', 'questions']);
15
+ export const VIEW_PARAM_TYPES = Object.freeze(['record', 'enum', 'text', 'number', 'date', 'period', 'boolean']);
16
+ const RELATIVE_DATES = ['today', 'tomorrow'];
17
+ const PERIODS = [...RELATIVE_DATES, 'this_week', 'last_week', 'this_month', 'last_month'];
18
+ const RESERVED_PARAMS = new Set(['record', 'assistantThread']);
19
+ const PAGE_ONLY = ['params', 'shows', 'memory', 'chrome', 'markdown', 'questions'];
20
+ const NAME = /^[a-z][A-Za-z0-9_]{0,63}$/;
21
+ // Credentials never ride in links, so no view param may look like one (the Portal drops these names).
22
+ const CREDENTIAL_PARAM = /^(?:jwt|code|state|key)$|token|secret|password|credential|session|auth|api_?key/i;
23
+ const WORD = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
24
+ const ID_TAIL = /(?:^|-)(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
25
+ const PLACEHOLDER = /^\{([a-z][A-Za-z0-9_]{0,63})(?:\.([^{}.\s][^{}]{0,199}))?\}$/;
26
+ const RECORD_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
27
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
28
+ const SERVICE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
29
+ // eslint-disable-next-line no-control-regex
30
+ const CONTROL = /[\u0000-\u001f\u007f]/;
31
+ const FIELD_KEYS = ['property', 'property_id', 'column'];
32
+ const OPS = new Set(['equals', 'not_equals', 'in', 'contains', 'not_contains', 'starts_with', 'ends_with', 'is_empty', 'is_not_empty',
33
+ 'greater_than', 'less_than', 'greater_than_or_equal_to', 'less_than_or_equal_to', 'does_not_contain', 'before', 'after',
34
+ 'on_or_before', 'on_or_after']);
35
+ // A record param's page rule (`page: { where }`) reads only these fields, with fixed values.
36
+ const PAGE_COLUMN_OPS = { record_key: ['equals', 'not_equals', 'in'], id: ['equals', 'not_equals', 'in'],
37
+ content_type: ['equals', 'not_equals', 'is_empty', 'is_not_empty'], file_type: ['equals', 'not_equals', 'is_empty', 'is_not_empty'],
38
+ title: ['equals', 'not_equals', 'is_empty', 'is_not_empty'] };
39
+ const PAGE_PROPERTY_OPS = { select: ['equals', 'not_equals', 'is_empty', 'is_not_empty'], checkbox: ['equals', 'not_equals'] };
40
+ const EMPTY_OPS = new Set(['is_empty', 'is_not_empty']);
41
+ const KIND_ALIASES = { phone: 'phone_number', string: 'text' };
42
+ const CRON_BOUNDS = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 7]];
43
+ const DEBOUNCE = [60, 86400];
44
+
45
+ const plain = value => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
46
+ const text = (value, limit) => typeof value === 'string' && value.trim().length >= 1 && value.trim().length <= limit && !CONTROL.test(value);
47
+ const isoDate = value => typeof value === 'string' && DATE.test(value) && !value.startsWith('0000-') && !Number.isNaN(Date.parse(`${value}T00:00:00Z`))
48
+ && new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value;
49
+ // A text filter value the database can store (no NUL, no lone surrogate), as native queries accept it.
50
+ const filterText = value => typeof value === 'string' && value.length <= 16384 && !value.includes('\u0000') && value.isWellFormed();
51
+ // Python's casefold, closely enough for property names ("ß" matches "ss").
52
+ const fold = value => String(value).toUpperCase().toLowerCase();
53
+
54
+ export function validateViewPath(value) {
55
+ if (typeof value !== 'string' || value.length < 1 || value.length > 100) {
56
+ throw usageError('Declare path as the words of this view\'s links, such as "notes" or "seo/keywords".');
57
+ }
58
+ const segments = value.split('/');
59
+ if (segments.length > 4 || segments.some(segment => !WORD.test(segment) || ID_TAIL.test(segment))) {
60
+ throw usageError('A path is up to four lowercase words (a-z, 0-9, dashes) separated by "/", without ids.');
61
+ }
62
+ if (RESERVED_PATHS.has(segments[0])) throw usageError(`The path "${segments[0]}" belongs to the Portal. Choose another first word.`);
63
+ return value;
64
+ }
65
+
66
+ function validCron(value) {
67
+ const fields = typeof value === 'string' ? value.split(' ') : [];
68
+ if (fields.length !== 5) return false;
69
+ return fields.every((field, index) => {
70
+ const [low, high] = CRON_BOUNDS[index];
71
+ const inRange = number => /^\d+$/.test(number) && Number(number) >= low && Number(number) <= high;
72
+ if (index === 0) return inRange(field);
73
+ return field.split(',').every(part => {
74
+ const [base, step, extra] = part.split('/');
75
+ if (extra !== undefined || (step !== undefined && !(/^\d+$/.test(step) && Number(step) >= 1 && Number(step) <= high))) return false;
76
+ if (base === '*') return true;
77
+ const [start, end, more] = base.split('-');
78
+ if (more !== undefined || !inRange(start) || (end !== undefined && (!inRange(end) || Number(start) > Number(end)))) return false;
79
+ return true;
80
+ });
81
+ });
82
+ }
83
+
84
+ function validateMemory(memory) {
85
+ if (!plain(memory) || Object.keys(memory).sort().join(',') !== 'attachments,markdown,screenshot,snapshots') {
86
+ throw usageError('Declare memory as { markdown, attachments, screenshot, snapshots }; see the memory guide.');
87
+ }
88
+ if (['markdown', 'attachments', 'screenshot'].some(name => typeof memory[name] !== 'boolean')) {
89
+ throw usageError('memory.markdown, memory.attachments and memory.screenshot are true or false.');
90
+ }
91
+ if (!Array.isArray(memory.snapshots) || memory.snapshots.length > 3) {
92
+ throw usageError('memory.snapshots lists on_render, { scheduled } and { on_change }, each at most once ([] for none).');
93
+ }
94
+ const seen = new Set();
95
+ for (const strategy of memory.snapshots) {
96
+ let kind;
97
+ if (strategy === 'on_render') kind = 'on_render';
98
+ else if (plain(strategy) && Object.keys(strategy).join(',') === 'scheduled') {
99
+ kind = 'scheduled';
100
+ if (!validCron(strategy.scheduled)) throw usageError('A scheduled snapshot uses a five-field UTC cron with a fixed minute, such as "0 7 * * 1".');
101
+ } else if (plain(strategy) && Object.keys(strategy).join(',') === 'on_change') {
102
+ kind = 'on_change';
103
+ const debounce = strategy.on_change?.debounceSeconds;
104
+ if (!plain(strategy.on_change) || Object.keys(strategy.on_change).join(',') !== 'debounceSeconds' || !Number.isInteger(debounce)
105
+ || debounce < DEBOUNCE[0] || debounce > DEBOUNCE[1]) {
106
+ throw usageError(`An on_change snapshot waits debounceSeconds (${DEBOUNCE[0]} to ${DEBOUNCE[1]}) after the last change.`);
107
+ }
108
+ } else throw usageError('memory.snapshots lists on_render, { scheduled: "<cron>" } or { on_change: { debounceSeconds } }.');
109
+ if (seen.has(kind)) throw usageError('Declare each memory snapshot strategy at most once.');
110
+ seen.add(kind);
111
+ }
112
+ }
113
+
114
+ /** One typed param value, as the SDK and the server accept it. */
115
+ export function viewParamValue(declaration, value) {
116
+ switch (declaration.type) {
117
+ case 'text': return typeof value === 'string' && value.length <= 500 && !CONTROL.test(value);
118
+ case 'enum': return typeof value === 'string' && (declaration.values || []).includes(value);
119
+ case 'number': return typeof value === 'number' && Number.isFinite(value);
120
+ case 'boolean': return typeof value === 'boolean';
121
+ case 'date': return RELATIVE_DATES.includes(value) || isoDate(value);
122
+ case 'period': {
123
+ if (PERIODS.includes(value)) return true;
124
+ const parts = typeof value === 'string' ? value.split('/') : [];
125
+ return parts.length === 2 && parts.every(isoDate) && parts[0] <= parts[1];
126
+ }
127
+ case 'record': return typeof value === 'string' && RECORD_KEY.test(value);
128
+ default: return false;
129
+ }
130
+ }
131
+
132
+ function validateParam(name, declaration, resources) {
133
+ if (!NAME.test(name) || RESERVED_PARAMS.has(name) || CREDENTIAL_PARAM.test(name)) throw usageError(`Param names are camelCase words; "${name}" is not available.`);
134
+ const allowed = new Set(['type', 'description', 'required', 'default', 'values', 'database', 'main', 'slugProperty', 'page', 'timezone']);
135
+ if (!plain(declaration) || Object.keys(declaration).some(field => !allowed.has(field)) || !VIEW_PARAM_TYPES.includes(declaration.type)) {
136
+ throw usageError(`Declare param "${name}" as { type, description, ... } with type ${VIEW_PARAM_TYPES.join(', ')}.`);
137
+ }
138
+ if (!text(declaration.description, 500)) throw usageError(`Describe param "${name}": what it selects, for agents and people.`);
139
+ if ('timezone' in declaration) {
140
+ try {
141
+ if (!['date', 'period'].includes(declaration.type) || typeof declaration.timezone !== 'string' || !declaration.timezone) throw new Error();
142
+ new Intl.DateTimeFormat('en', { timeZone: declaration.timezone });
143
+ } catch { throw usageError(`Param "${name}": timezone is an IANA timezone for a date or period.`); }
144
+ }
145
+ if ('required' in declaration && typeof declaration.required !== 'boolean') throw usageError(`Param "${name}": required is true or false.`);
146
+ if (declaration.type === 'enum') {
147
+ const values = declaration.values;
148
+ if (!Array.isArray(values) || values.length < 1 || values.length > 100 || new Set(values).size !== values.length
149
+ || values.some(value => !text(value, 100))) throw usageError(`Param "${name}": an enum lists its distinct values.`);
150
+ } else if ('values' in declaration) throw usageError(`Param "${name}": only enum params list values.`);
151
+ if (declaration.type === 'record') {
152
+ if (resources[declaration.database]?.kind !== 'database') {
153
+ throw usageError(`Param "${name}": a record param names a database this Space declares in resources.`);
154
+ }
155
+ if ('main' in declaration && typeof declaration.main !== 'boolean') throw usageError(`Param "${name}": main is true or false.`);
156
+ if ('slugProperty' in declaration && !text(declaration.slugProperty, 200)) throw usageError(`Param "${name}": slugProperty names a property of its database.`);
157
+ if ('default' in declaration) throw usageError(`Param "${name}": a record param has no default record.`);
158
+ if ('page' in declaration) {
159
+ if (declaration.required === true) throw usageError(`Param "${name}": a page param is optional, so Back opens its view without it.`);
160
+ validatePage(name, declaration.page, resources[declaration.database]);
161
+ }
162
+ } else if (['database', 'main', 'slugProperty', 'page'].some(field => field in declaration)) {
163
+ throw usageError(`Param "${name}": database, main, slugProperty and page belong to record params.`);
164
+ }
165
+ if ('default' in declaration) {
166
+ if (declaration.required === true) throw usageError(`Param "${name}": a required param has no default.`);
167
+ if (!viewParamValue(declaration, declaration.default)) throw usageError(`Param "${name}": its default must be a valid ${declaration.type} value.`);
168
+ }
169
+ }
170
+
171
+ function checkPlaceholder(value, params, collection, where) {
172
+ const match = typeof value === 'string' ? PLACEHOLDER.exec(value) : null;
173
+ if (!match) return;
174
+ const [, name, property] = match;
175
+ if (name === 'record') {
176
+ const records = Object.values(params).filter(declaration => declaration.type === 'record');
177
+ if (records.length === 1 || (!records.length && collection)) return;
178
+ throw usageError('Use {record} only in a view with exactly one record param (or a collection); otherwise name the param, such as {project}.');
179
+ }
180
+ if (!Object.hasOwn(params, name)) throw usageError(`${where}: "{${name}}" names no declared param.`);
181
+ if (params[name].type === 'period') {
182
+ if (!['start', 'end'].includes(property)) throw usageError(`${where}: compare {${name}.start} or {${name}.end} with a date.`);
183
+ return;
184
+ }
185
+ if (property !== undefined && params[name].type !== 'record') throw usageError(`${where}: only record params have properties, such as {${name}}.`);
186
+ }
187
+
188
+ function walkFilter(node, visit, where, depth = 0, budget = { left: 200 }) {
189
+ budget.left -= 1;
190
+ if (depth > 12 || budget.left < 0) throw usageError(`${where}: the filter is too complex; use fewer conditions.`);
191
+ if (depth === 0 && plain(node) && !Object.keys(node).length) return;
192
+ if (!plain(node)) throw usageError(`${where}: a filter is an object.`);
193
+ for (const group of ['and', 'or']) {
194
+ if (!Object.hasOwn(node, group)) continue;
195
+ const children = node[group];
196
+ if (Object.keys(node).length !== 1 || !Array.isArray(children) || !children.length) throw usageError(`${where}: filter groups need at least one condition.`);
197
+ children.forEach(child => walkFilter(child, visit, where, depth + 1, budget));
198
+ return;
199
+ }
200
+ const field = node.field;
201
+ if (Object.keys(node).some(key => !['field', 'op', 'value', 'when'].includes(key)) || !('field' in node) || !('op' in node)
202
+ || !plain(field) || Object.keys(field).length !== 1 || !FIELD_KEYS.includes(Object.keys(field)[0])
203
+ || typeof Object.values(field)[0] !== 'string' || !OPS.has(node.op)) {
204
+ throw usageError(`${where}: each condition is { field: { property }, op, value } in the native filter language.`);
205
+ }
206
+ visit(node);
207
+ }
208
+
209
+ /**
210
+ * The kind of a property of a database this release creates, resolved as the server does (property ID, else
211
+ * current then previous name, never ambiguous); undefined when the CLI has no schema (a linked database).
212
+ */
213
+ function createdPropertyKind(resource, field, where) {
214
+ const schema = resource?.create?.schema;
215
+ if (!plain(schema) || !plain(schema.properties) || !Array.isArray(schema.property_order) || schema.schema_version == null) return undefined;
216
+ const properties = Object.entries(schema.properties).filter(([, prop]) => plain(prop));
217
+ let keys;
218
+ if ('property_id' in field) keys = properties.filter(([key]) => key === field.property_id).map(([key]) => key);
219
+ else {
220
+ const name = fold(field.property);
221
+ keys = properties.filter(([key, prop]) => fold(prop.name ?? key) === name).map(([key]) => key);
222
+ if (!keys.length) {
223
+ keys = properties.filter(([, prop]) => Array.isArray(prop.previous_names) && prop.previous_names.some(old => fold(old) === name)).map(([key]) => key);
224
+ }
225
+ }
226
+ if (keys.length !== 1) throw usageError(`${where}: "${Object.values(field)[0]}" is not a property of that database.`);
227
+ const prop = schema.properties[keys[0]];
228
+ const declared = String(prop.kind ?? prop.type ?? 'rich_text');
229
+ const kind = Object.hasOwn(KIND_ALIASES, declared) ? KIND_ALIASES[declared] : declared;
230
+ return kind === 'status' ? 'select' : kind;
231
+ }
232
+
233
+ /**
234
+ * A record param's page rule: `{ where }` over the subset a page reads (record_key, id, content_type, file_type and
235
+ * title columns; select, status and checkbox properties), with fixed values. The server re-checks properties against
236
+ * the bound schema at verify and deploy and never sends the rule to the browser.
237
+ */
238
+ function validatePage(name, page, resource) {
239
+ const where = `params.${name}.page`;
240
+ if (!plain(page) || Object.keys(page).join(',') !== 'where' || !plain(page.where) || !Object.keys(page.where).length) {
241
+ throw usageError(`Param "${name}": declare page as { where } with at least one condition over its records.`);
242
+ }
243
+ walkFilter(page.where, condition => {
244
+ const [[key, reference]] = Object.entries(condition.field);
245
+ const kind = key === 'column' ? 'column' : createdPropertyKind(resource, condition.field, where);
246
+ const ops = kind === 'column' ? (Object.hasOwn(PAGE_COLUMN_OPS, reference) ? PAGE_COLUMN_OPS[reference] : null)
247
+ : kind === undefined ? PAGE_PROPERTY_OPS.select : Object.hasOwn(PAGE_PROPERTY_OPS, kind) ? PAGE_PROPERTY_OPS[kind] : null;
248
+ if (!ops) {
249
+ throw usageError(kind === 'column'
250
+ ? `${where}: a page rule reads the columns ${Object.keys(PAGE_COLUMN_OPS).join(', ')}, and select, status or checkbox properties.`
251
+ : `${where}: "${reference}" is not a select, status or checkbox property, so a page rule cannot read it.`);
252
+ }
253
+ if (!ops.includes(condition.op)) throw usageError(`${where}: ${condition.op} does not apply to "${reference}" in a page rule.`);
254
+ if (EMPTY_OPS.has(condition.op)) {
255
+ if ('value' in condition) throw usageError(`${where}: ${condition.op} takes no value.`);
256
+ return;
257
+ }
258
+ if (!('value' in condition)) throw usageError(`${where}: ${condition.op} needs a value.`);
259
+ if (condition.op === 'in' && (!Array.isArray(condition.value) || condition.value.length < 1 || condition.value.length > 500)) {
260
+ throw usageError(`${where}: in takes a list of 1 to 500 record ids.`);
261
+ }
262
+ for (const value of condition.op === 'in' ? condition.value : [condition.value]) {
263
+ // Fixed values only: neither {param} nor {{param}} is ever bound in a page rule (the server refuses both).
264
+ if (typeof value === 'string' && (PLACEHOLDER.test(value)
265
+ || (value.startsWith('{{') && value.endsWith('}}') && PLACEHOLDER.test(value.slice(1, -1))))) {
266
+ throw usageError(`${where}: a page rule compares with fixed values, not {param} placeholders.`);
267
+ }
268
+ const [valid, expected] = kind === 'checkbox' ? [typeof value === 'boolean', 'true or false']
269
+ : kind === undefined ? [typeof value === 'boolean' || filterText(value), 'an option name, or true or false for a checkbox']
270
+ : [filterText(value), kind === 'select' ? 'an option name or ID' : 'a text value'];
271
+ if (!valid) throw usageError(`${where}: compare "${reference}" with ${expected}.`);
272
+ }
273
+ }, where);
274
+ }
275
+
276
+ function validateShows(name, declaration, params, resources, collection) {
277
+ const where = `shows.${name}`;
278
+ if (!NAME.test(name)) throw usageError(`Shows names are camelCase words; "${name}" is not available.`);
279
+ if (plain(declaration) && 'about' in declaration) {
280
+ const services = declaration.services ?? [];
281
+ if (Object.keys(declaration).some(field => !['about', 'services'].includes(field)) || !text(declaration.about, 500)
282
+ || !Array.isArray(services) || services.length > 20 || services.some(service => typeof service !== 'string' || !SERVICE.test(service))) {
283
+ throw usageError(`${where}: describe what it shows as { about, services? }.`);
284
+ }
285
+ return;
286
+ }
287
+ if (!plain(declaration) || Object.keys(declaration).some(field => !['database', 'where', 'open'].includes(field)) || !('where' in declaration)) {
288
+ throw usageError(`${where}: declare { database, where, open? } for Notis data or { about, services? } otherwise.`);
289
+ }
290
+ if (resources[declaration.database]?.kind !== 'database') {
291
+ throw usageError(`${where}: "${declaration.database}" is not a database this Space declares and links.`);
292
+ }
293
+ if (declaration.open !== undefined && (params[declaration.open]?.type !== 'record' || params[declaration.open].database !== declaration.database)) {
294
+ throw usageError(`${where}: open names a record param over the same database.`);
295
+ }
296
+ walkFilter(declaration.where, condition => {
297
+ if ('when' in condition && (typeof condition.when !== 'string' || params[condition.when]?.type !== 'boolean')) {
298
+ throw usageError(`${where}: when names a declared boolean param.`);
299
+ }
300
+ if (!('value' in condition)) return;
301
+ const values = condition.op === 'in' && Array.isArray(condition.value) ? condition.value : [condition.value];
302
+ values.forEach(value => checkPlaceholder(value, params, collection, where));
303
+ }, where);
304
+ }
305
+
306
+ /**
307
+ * Validates V4 declarations independently of the optional version marker, then returns
308
+ * their manifest form without inventing a version or page fields for containers.
309
+ */
310
+ export function viewManifestFields(definition, { entry, resources }) {
311
+ const container = !entry;
312
+ const version = definition.specVersion;
313
+ if (version !== undefined && version !== VIEW_SPEC_VERSION) throw usageError(`specVersion is ${VIEW_SPEC_VERSION} for sources that declare the view manifest.`);
314
+ if (container && PAGE_ONLY.some(field => definition[field] !== undefined)) {
315
+ throw usageError('A container has no page: params, shows, memory, chrome and markdown belong to its views.');
316
+ }
317
+ if (!container || version === VIEW_SPEC_VERSION) {
318
+ const missing = ['path', 'description'].filter(field => !definition[field]);
319
+ if (!container && definition.memory === undefined) missing.push('memory');
320
+ if (missing.length) throw usageError(`This view needs ${missing.join(', ')} (specVersion 2).`);
321
+ if (!text(definition.description, 300)) throw usageError('Describe this view in one line (up to 300 characters).');
322
+ }
323
+ if (definition.path !== undefined) validateViewPath(definition.path);
324
+ if (definition.questions !== undefined && (!Array.isArray(definition.questions) || !definition.questions.length
325
+ || definition.questions.length > 30 || definition.questions.some(value => !text(value, 300))
326
+ || new Set(definition.questions).size !== definition.questions.length)) {
327
+ throw usageError('List up to 30 distinct questions this view answers (300 characters each).');
328
+ }
329
+ const params = definition.params ?? {};
330
+ if (definition.params !== undefined) {
331
+ if (!plain(params) || Object.keys(params).length > 50) throw usageError('Declare params as { name: { type, description, ... } }.');
332
+ const mains = new Map();
333
+ let page = null;
334
+ for (const [name, declaration] of Object.entries(params)) {
335
+ validateParam(name, declaration, resources);
336
+ if ('page' in declaration) {
337
+ if (page) throw usageError(`Params "${page}" and "${name}" both declare page; at most one param of a view opens records as pages.`);
338
+ page = name;
339
+ }
340
+ if (declaration.type !== 'record' || declaration.main !== true) continue;
341
+ if (mains.has(declaration.database)) {
342
+ throw usageError(`Params "${mains.get(declaration.database)}" and "${name}" both claim main for database "${declaration.database}"; a database has one main view.`);
343
+ }
344
+ mains.set(declaration.database, name);
345
+ }
346
+ }
347
+ if (definition.shows !== undefined) {
348
+ if (!plain(definition.shows) || Object.keys(definition.shows).length > 50) {
349
+ throw usageError('Declare shows as { name: { database, where, open? } } or { name: { about, services? } }.');
350
+ }
351
+ for (const [name, declaration] of Object.entries(definition.shows)) validateShows(name, declaration, params, resources, definition.collection);
352
+ }
353
+ if (definition.memory !== undefined) validateMemory(definition.memory);
354
+ if (definition.chrome !== undefined && !['portal', 'hidden'].includes(definition.chrome)) throw usageError("chrome is 'portal' (default) or 'hidden'.");
355
+ if (definition.markdown !== undefined && (typeof definition.markdown !== 'string' || !definition.markdown)) {
356
+ throw usageError('markdown names a source module whose default export renders this view as Markdown.');
357
+ }
358
+ return {
359
+ ...(version !== undefined ? { spec_version: version } : {}),
360
+ ...(definition.path !== undefined ? { path: definition.path } : {}),
361
+ ...(definition.questions !== undefined ? { questions: definition.questions } : {}),
362
+ ...(definition.params !== undefined ? { params } : {}),
363
+ ...(definition.shows !== undefined ? { shows: definition.shows } : {}),
364
+ ...(definition.memory !== undefined ? { memory: definition.memory } : {}),
365
+ ...(definition.chrome !== undefined ? { chrome: definition.chrome } : {}),
366
+ ...(definition.markdown !== undefined ? { markdown: { export_name: 'SpaceMarkdown' } } : {}),
367
+ };
368
+ }
@@ -3,6 +3,8 @@ import type { BaseSkillReconcileResult } from './base-skills.js';
3
3
  export interface ReconcileAllSkillsResult {
4
4
  baseSkills: string[];
5
5
  baseInstalled: number;
6
+ /** Base skills left as installed because that copy is newer. */
7
+ baseKept: number;
6
8
  baseLinked: number;
7
9
  baseBackups: string[];
8
10
  }
@@ -30,8 +32,10 @@ export function reconcileAllSkills<T extends object>(options: {
30
32
  serverUrl: string,
31
33
  jwt: string,
32
34
  dependencies: Record<string, never>,
33
- options: { honorSyncEnabled: boolean },
35
+ options: { honorSyncEnabled: boolean; syncHome?: string; linkAgentFolders?: boolean },
34
36
  ) => Promise<T>;
35
37
  reconcileBase?: (options: { userId?: string | null; home?: string }) => BaseSkillReconcileResult;
36
38
  lockOptions?: Omit<SkillSyncLockOptions, 'home'>;
39
+ /** False syncs the account's own folder only; another account owns the agent folders. */
40
+ linkAgentFolders?: boolean;
37
41
  }): Promise<T & ReconcileAllSkillsResult>;
@@ -211,19 +211,28 @@ export async function reconcileAllSkills({
211
211
  runAccountSync,
212
212
  reconcileBase = reconcileBaseSkills,
213
213
  lockOptions = {},
214
+ linkAgentFolders = true,
214
215
  }) {
215
216
  return withSkillSyncLock(async () => {
216
- const base = reconcileBase({ userId, ...(home ? { home } : {}) });
217
+ // Base skills are linked from the account that owns the agent folders.
218
+ const base = linkAgentFolders
219
+ ? reconcileBase({ userId, ...(home ? { home } : {}) })
220
+ : { skills: [], installed: 0, kept: 0, linked: 0, unchanged: 0, backups: [] };
217
221
  const account = await runAccountSync(
218
222
  serverUrl,
219
223
  jwt,
220
224
  {},
221
- { honorSyncEnabled },
225
+ {
226
+ honorSyncEnabled,
227
+ ...(home ? { syncHome: home } : {}),
228
+ ...(linkAgentFolders ? {} : { linkAgentFolders: false }),
229
+ },
222
230
  );
223
231
  return {
224
232
  ...account,
225
233
  baseSkills: base.skills,
226
234
  baseInstalled: base.installed,
235
+ baseKept: base.kept ?? 0,
227
236
  baseLinked: base.linked,
228
237
  baseBackups: base.backups,
229
238
  };
@@ -0,0 +1,138 @@
1
+ import { createInterface } from 'node:readline';
2
+
3
+ // A tool that changes something costly (for example LOCAL_NOTIS_MANAGE_CLOUD_COMPUTER)
4
+ // first answers with a preview: {requires_confirmation: true, summary,
5
+ // confirm_arguments, confirm_input?}. `notis tools exec` shows it, asks
6
+ // "Continue? [y/N]" and calls the tool again with confirm_arguments.
7
+
8
+ function parseJsonObject(value) {
9
+ if (typeof value !== 'string') return null;
10
+ const trimmed = value.trim();
11
+ if (!trimmed.startsWith('{')) return null;
12
+ try {
13
+ const parsed = JSON.parse(trimmed);
14
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ function asObject(value) {
21
+ if (value && typeof value === 'object' && !Array.isArray(value)) return value;
22
+ return parseJsonObject(value);
23
+ }
24
+
25
+ // The single tool result inside a COMPOSIO_MULTI_EXECUTE_TOOL answer, or the
26
+ // payload itself when it already is one.
27
+ export function extractToolResult(payload) {
28
+ const candidates = [
29
+ payload?.data?.data?.results?.[0]?.response?.data,
30
+ payload?.data?.results?.[0]?.response?.data,
31
+ payload?.results?.[0]?.response?.data,
32
+ payload?.data?.result,
33
+ payload?.result,
34
+ payload,
35
+ ];
36
+ for (const candidate of candidates) {
37
+ const value = asObject(candidate);
38
+ if (value) return value;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ export function findConfirmationPreview(payload) {
44
+ const result = extractToolResult(payload);
45
+ if (!result || result.requires_confirmation !== true) return null;
46
+ const confirmArguments = asObject(result.confirm_arguments);
47
+ if (!confirmArguments) return null;
48
+ return { ...result, confirm_arguments: confirmArguments };
49
+ }
50
+
51
+ export function renderPreview(preview) {
52
+ const summary = Array.isArray(preview.summary)
53
+ ? preview.summary.filter((line) => typeof line === 'string' && line)
54
+ : (typeof preview.summary === 'string' && preview.summary ? [preview.summary] : []);
55
+ const lines = summary.length ? summary : ['This change needs your confirmation.'];
56
+ return lines.join('\n');
57
+ }
58
+
59
+ export function isYes(answer) {
60
+ return /^\s*y(es)?\s*$/i.test(String(answer ?? ''));
61
+ }
62
+
63
+ // Decides what to do with a preview. Returns one of:
64
+ // {decision: 'apply', arguments} call the tool again with these
65
+ // {decision: 'declined', reason} the person said no
66
+ // {decision: 'needs_input', field, reason} a required typed value is missing
67
+ // {decision: 'not_interactive'} no prompt possible: show the preview only
68
+ export async function resolveConfirmation({ preview, args = {}, yes = false, interactive = false, ask }) {
69
+ const confirmArguments = { ...preview.confirm_arguments };
70
+ const confirmInput = asObject(preview.confirm_input);
71
+
72
+ if (confirmInput && typeof confirmInput.field === 'string' && confirmInput.field) {
73
+ const { field } = confirmInput;
74
+ let value = args[field] ?? confirmArguments[field];
75
+ if (value === undefined || value === null || value === '') {
76
+ if (yes || !interactive) {
77
+ return {
78
+ decision: 'needs_input',
79
+ field,
80
+ reason: `Add "${field}" to --arguments${confirmInput.expected ? ` (it must be ${JSON.stringify(confirmInput.expected)})` : ''} to apply this change.`,
81
+ };
82
+ }
83
+ value = (await ask(`${confirmInput.prompt || `Type ${field} to continue`}: `)).trim();
84
+ if (!value) return { decision: 'declined', reason: 'Cancelled. Nothing changed.' };
85
+ }
86
+ if (typeof confirmInput.expected === 'string' && value !== confirmInput.expected) {
87
+ return {
88
+ decision: 'declined',
89
+ reason: `That does not match ${JSON.stringify(confirmInput.expected)}. Nothing changed.`,
90
+ };
91
+ }
92
+ confirmArguments[field] = value;
93
+ }
94
+
95
+ if (yes) return { decision: 'apply', arguments: confirmArguments };
96
+ if (!interactive) return { decision: 'not_interactive' };
97
+ const answer = await ask('Continue? [y/N] ');
98
+ if (!isYes(answer)) return { decision: 'declined', reason: 'Cancelled. Nothing changed.' };
99
+ return { decision: 'apply', arguments: confirmArguments };
100
+ }
101
+
102
+ export function canPrompt({ runtime, output } = {}) {
103
+ if (runtime?.nonInteractive) return false;
104
+ if (output?.isMachineMode?.()) return false;
105
+ // Lets the CLI tests drive the prompt through a pipe.
106
+ if (process.env.NODE_ENV === 'test' && process.env.NOTIS_TEST_ASSUME_TTY === '1') return true;
107
+ return Boolean(process.stdin.isTTY && process.stderr.isTTY);
108
+ }
109
+
110
+ // Asks on stderr so stdout keeps only the command's result. One reader serves
111
+ // every question of a confirmation and queues the lines, so answers piped in
112
+ // together ("data\ny\n") reach their questions in order. Call close() after.
113
+ export function createStderrPrompter({ input = process.stdin, output = process.stderr } = {}) {
114
+ const rl = createInterface({ input, output, terminal: Boolean(input.isTTY && output.isTTY) });
115
+ const lines = [];
116
+ const waiting = [];
117
+ let ended = false;
118
+ rl.on('line', (line) => {
119
+ const next = waiting.shift();
120
+ if (next) next(line);
121
+ else lines.push(line);
122
+ });
123
+ rl.on('close', () => {
124
+ ended = true;
125
+ while (waiting.length) waiting.shift()('');
126
+ });
127
+ return {
128
+ ask(question) {
129
+ output.write(question);
130
+ if (lines.length) return Promise.resolve(lines.shift());
131
+ if (ended) return Promise.resolve('');
132
+ return new Promise((resolveAnswer) => waiting.push(resolveAnswer));
133
+ },
134
+ close() {
135
+ rl.close();
136
+ },
137
+ };
138
+ }