@xdelivered/emberflow 0.2.0

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 (414) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/bin/commands.test.ts +185 -0
  4. package/bin/commands.ts +306 -0
  5. package/bin/emberflow.mjs +38 -0
  6. package/bin/init.test.ts +165 -0
  7. package/bin/init.ts +297 -0
  8. package/bin/runtime.mjs +105 -0
  9. package/bin/runtime.test.ts +167 -0
  10. package/bin/skillTargets.test.ts +108 -0
  11. package/bin/skillTargets.ts +36 -0
  12. package/bin/skillTemplates.test.ts +37 -0
  13. package/bin/tsconfig.json +23 -0
  14. package/dist/bin/commands.d.ts +24 -0
  15. package/dist/bin/commands.js +252 -0
  16. package/dist/bin/init.d.ts +17 -0
  17. package/dist/bin/init.js +241 -0
  18. package/dist/bin/runtime.d.mts +39 -0
  19. package/dist/bin/runtime.mjs +96 -0
  20. package/dist/bin/skillTargets.d.ts +13 -0
  21. package/dist/bin/skillTargets.js +22 -0
  22. package/dist/server/agents/claudeAdapter.d.ts +8 -0
  23. package/dist/server/agents/claudeAdapter.js +190 -0
  24. package/dist/server/agents/claudeParse.d.ts +2 -0
  25. package/dist/server/agents/claudeParse.js +162 -0
  26. package/dist/server/agents/codexAdapter.d.ts +13 -0
  27. package/dist/server/agents/codexAdapter.js +174 -0
  28. package/dist/server/agents/codexParse.d.ts +2 -0
  29. package/dist/server/agents/codexParse.js +74 -0
  30. package/dist/server/agents/detect.d.ts +23 -0
  31. package/dist/server/agents/detect.js +35 -0
  32. package/dist/server/agents/gitScope.d.ts +24 -0
  33. package/dist/server/agents/gitScope.js +95 -0
  34. package/dist/server/agents/modelRejectionHint.d.ts +8 -0
  35. package/dist/server/agents/modelRejectionHint.js +12 -0
  36. package/dist/server/agents/prompt.d.ts +79 -0
  37. package/dist/server/agents/prompt.js +364 -0
  38. package/dist/server/agents/runManager.d.ts +90 -0
  39. package/dist/server/agents/runManager.js +259 -0
  40. package/dist/server/agents/types.d.ts +15 -0
  41. package/dist/server/agents/types.js +1 -0
  42. package/dist/server/apiStore.d.ts +60 -0
  43. package/dist/server/apiStore.js +249 -0
  44. package/dist/server/auth/enforce.d.ts +1 -0
  45. package/dist/server/auth/enforce.js +6 -0
  46. package/dist/server/auth/types.d.ts +1 -0
  47. package/dist/server/auth/types.js +1 -0
  48. package/dist/server/auth/verifiers.d.ts +1 -0
  49. package/dist/server/auth/verifiers.js +4 -0
  50. package/dist/server/authAttach.d.ts +19 -0
  51. package/dist/server/authAttach.js +79 -0
  52. package/dist/server/cli.d.ts +7 -0
  53. package/dist/server/cli.js +804 -0
  54. package/dist/server/client.d.ts +130 -0
  55. package/dist/server/client.js +247 -0
  56. package/dist/server/environments.d.ts +103 -0
  57. package/dist/server/environments.js +486 -0
  58. package/dist/server/flowStore.d.ts +37 -0
  59. package/dist/server/flowStore.js +117 -0
  60. package/dist/server/httpOperations.d.ts +32 -0
  61. package/dist/server/httpOperations.js +62 -0
  62. package/dist/server/index.d.ts +1 -0
  63. package/dist/server/index.js +1022 -0
  64. package/dist/server/infrastructure.d.ts +51 -0
  65. package/dist/server/infrastructure.js +116 -0
  66. package/dist/server/login.d.ts +21 -0
  67. package/dist/server/login.js +44 -0
  68. package/dist/server/mcp.d.ts +1 -0
  69. package/dist/server/mcp.js +182 -0
  70. package/dist/server/migrateFlows.d.ts +8 -0
  71. package/dist/server/migrateFlows.js +25 -0
  72. package/dist/server/mockHandler.d.ts +15 -0
  73. package/dist/server/mockHandler.js +35 -0
  74. package/dist/server/nodesPayload.d.ts +14 -0
  75. package/dist/server/nodesPayload.js +14 -0
  76. package/dist/server/normalizeFlow.d.ts +21 -0
  77. package/dist/server/normalizeFlow.js +48 -0
  78. package/dist/server/openBrowser.d.ts +8 -0
  79. package/dist/server/openBrowser.js +16 -0
  80. package/dist/server/operationResult.d.ts +12 -0
  81. package/dist/server/operationResult.js +26 -0
  82. package/dist/server/pathGuard.d.ts +1 -0
  83. package/dist/server/pathGuard.js +16 -0
  84. package/dist/server/pathSafety.d.ts +20 -0
  85. package/dist/server/pathSafety.js +35 -0
  86. package/dist/server/projectConfig.d.ts +47 -0
  87. package/dist/server/projectConfig.js +61 -0
  88. package/dist/server/projectMode.d.ts +26 -0
  89. package/dist/server/projectMode.js +44 -0
  90. package/dist/server/redact.d.ts +6 -0
  91. package/dist/server/redact.js +37 -0
  92. package/dist/server/runRegistry.d.ts +84 -0
  93. package/dist/server/runRegistry.js +200 -0
  94. package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
  95. package/dist/server/runtime/RuntimeAdapter.js +1 -0
  96. package/dist/server/runtime/expressAdapter.d.ts +19 -0
  97. package/dist/server/runtime/expressAdapter.js +56 -0
  98. package/dist/server/scenarioTest.d.ts +14 -0
  99. package/dist/server/scenarioTest.js +66 -0
  100. package/dist/server/subflowRunner.d.ts +32 -0
  101. package/dist/server/subflowRunner.js +72 -0
  102. package/dist/server/testRunner.d.ts +70 -0
  103. package/dist/server/testRunner.js +151 -0
  104. package/dist/src/config.d.ts +3 -0
  105. package/dist/src/config.js +2 -0
  106. package/dist/src/engine/authVerify.d.ts +33 -0
  107. package/dist/src/engine/authVerify.js +63 -0
  108. package/dist/src/engine/diagnostics.d.ts +62 -0
  109. package/dist/src/engine/diagnostics.js +148 -0
  110. package/dist/src/engine/executor.d.ts +151 -0
  111. package/dist/src/engine/executor.js +684 -0
  112. package/dist/src/engine/httpError.d.ts +7 -0
  113. package/dist/src/engine/httpError.js +14 -0
  114. package/dist/src/engine/index.d.ts +11 -0
  115. package/dist/src/engine/index.js +11 -0
  116. package/dist/src/engine/output.d.ts +11 -0
  117. package/dist/src/engine/output.js +20 -0
  118. package/dist/src/engine/publish.d.ts +30 -0
  119. package/dist/src/engine/publish.js +95 -0
  120. package/dist/src/engine/registry.d.ts +34 -0
  121. package/dist/src/engine/registry.js +67 -0
  122. package/dist/src/engine/scenarios.d.ts +12 -0
  123. package/dist/src/engine/scenarios.js +19 -0
  124. package/dist/src/engine/schemaCheck.d.ts +4 -0
  125. package/dist/src/engine/schemaCheck.js +34 -0
  126. package/dist/src/engine/trace.d.ts +12 -0
  127. package/dist/src/engine/trace.js +12 -0
  128. package/dist/src/engine/types.d.ts +308 -0
  129. package/dist/src/engine/types.js +1 -0
  130. package/dist/src/engine/validation.d.ts +26 -0
  131. package/dist/src/engine/validation.js +318 -0
  132. package/dist/src/flows/anomaly-flows.d.ts +23 -0
  133. package/dist/src/flows/anomaly-flows.js +151 -0
  134. package/dist/src/flows/city-sweep-flow.d.ts +10 -0
  135. package/dist/src/flows/city-sweep-flow.js +127 -0
  136. package/dist/src/flows/login-flow.d.ts +13 -0
  137. package/dist/src/flows/login-flow.js +145 -0
  138. package/dist/src/flows/pradar-flows.d.ts +5 -0
  139. package/dist/src/flows/pradar-flows.js +375 -0
  140. package/dist/src/flows/weather-flow.d.ts +12 -0
  141. package/dist/src/flows/weather-flow.js +150 -0
  142. package/dist/src/lib/pathParams.d.ts +3 -0
  143. package/dist/src/lib/pathParams.js +6 -0
  144. package/dist/src/nodes/anomaly.d.ts +2 -0
  145. package/dist/src/nodes/anomaly.js +556 -0
  146. package/dist/src/nodes/flow-control.d.ts +7 -0
  147. package/dist/src/nodes/flow-control.js +270 -0
  148. package/dist/src/nodes/index.d.ts +3 -0
  149. package/dist/src/nodes/index.js +20 -0
  150. package/dist/src/nodes/login.d.ts +7 -0
  151. package/dist/src/nodes/login.js +158 -0
  152. package/dist/src/nodes/loops.d.ts +11 -0
  153. package/dist/src/nodes/loops.js +60 -0
  154. package/dist/src/nodes/pradar/index.d.ts +7 -0
  155. package/dist/src/nodes/pradar/index.js +9 -0
  156. package/dist/src/nodes/pradar/logic.d.ts +189 -0
  157. package/dist/src/nodes/pradar/logic.js +370 -0
  158. package/dist/src/nodes/pradar/nodes.d.ts +42 -0
  159. package/dist/src/nodes/pradar/nodes.js +801 -0
  160. package/dist/src/nodes/requireAuth.d.ts +20 -0
  161. package/dist/src/nodes/requireAuth.js +48 -0
  162. package/dist/src/nodes/response.d.ts +8 -0
  163. package/dist/src/nodes/response.js +33 -0
  164. package/dist/src/nodes/weather.d.ts +33 -0
  165. package/dist/src/nodes/weather.js +260 -0
  166. package/package.json +92 -0
  167. package/server/agentRoute.test.ts +157 -0
  168. package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
  169. package/server/agents/__fixtures__/fake-claude.mjs +55 -0
  170. package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
  171. package/server/agents/__fixtures__/fake-codex.mjs +52 -0
  172. package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
  173. package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
  174. package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
  175. package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
  176. package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
  177. package/server/agents/claudeAdapter.test.ts +127 -0
  178. package/server/agents/claudeAdapter.ts +212 -0
  179. package/server/agents/claudeParse.ts +193 -0
  180. package/server/agents/codexAdapter.test.ts +125 -0
  181. package/server/agents/codexAdapter.ts +198 -0
  182. package/server/agents/codexParse.test.ts +58 -0
  183. package/server/agents/codexParse.ts +126 -0
  184. package/server/agents/detect.test.ts +65 -0
  185. package/server/agents/detect.ts +43 -0
  186. package/server/agents/gitScope.test.ts +129 -0
  187. package/server/agents/gitScope.ts +110 -0
  188. package/server/agents/modelRejectionHint.test.ts +30 -0
  189. package/server/agents/modelRejectionHint.ts +14 -0
  190. package/server/agents/prompt.test.ts +613 -0
  191. package/server/agents/prompt.ts +527 -0
  192. package/server/agents/runManager.test.ts +251 -0
  193. package/server/agents/runManager.ts +286 -0
  194. package/server/agents/types.ts +17 -0
  195. package/server/apiMount.test.ts +591 -0
  196. package/server/apiStore.test.ts +324 -0
  197. package/server/apiStore.ts +241 -0
  198. package/server/auth/enforce.test.ts +61 -0
  199. package/server/auth/enforce.ts +6 -0
  200. package/server/auth/types.ts +5 -0
  201. package/server/auth/verifiers.test.ts +37 -0
  202. package/server/auth/verifiers.ts +9 -0
  203. package/server/authAttach.test.ts +175 -0
  204. package/server/authAttach.ts +99 -0
  205. package/server/cli.ts +847 -0
  206. package/server/cliAuth.test.ts +229 -0
  207. package/server/cliCreate.test.ts +131 -0
  208. package/server/cliDoctor.test.ts +326 -0
  209. package/server/cliTest.test.ts +735 -0
  210. package/server/client.ts +314 -0
  211. package/server/demoProject.test.ts +29 -0
  212. package/server/diagnosticsRoute.test.ts +169 -0
  213. package/server/environments.test.ts +620 -0
  214. package/server/environments.ts +603 -0
  215. package/server/environmentsRoute.test.ts +385 -0
  216. package/server/errorWorkflow.test.ts +205 -0
  217. package/server/flowStore.test.ts +84 -0
  218. package/server/flowStore.ts +125 -0
  219. package/server/httpOperations.test.ts +377 -0
  220. package/server/httpOperations.ts +96 -0
  221. package/server/index.ts +1111 -0
  222. package/server/infrastructure.test.ts +154 -0
  223. package/server/infrastructure.ts +161 -0
  224. package/server/infrastructureRoute.test.ts +105 -0
  225. package/server/login.test.ts +153 -0
  226. package/server/login.ts +67 -0
  227. package/server/mcp.ts +278 -0
  228. package/server/migrateFlows.test.ts +55 -0
  229. package/server/migrateFlows.ts +23 -0
  230. package/server/mockHandler.test.ts +138 -0
  231. package/server/mockHandler.ts +52 -0
  232. package/server/mockRun.test.ts +367 -0
  233. package/server/mockServe.test.ts +467 -0
  234. package/server/nodesPayload.test.ts +29 -0
  235. package/server/nodesPayload.ts +26 -0
  236. package/server/normalizeFlow.test.ts +80 -0
  237. package/server/normalizeFlow.ts +59 -0
  238. package/server/openBrowser.test.ts +20 -0
  239. package/server/openBrowser.ts +22 -0
  240. package/server/operationResult.test.ts +67 -0
  241. package/server/operationResult.ts +27 -0
  242. package/server/pathGuard.test.ts +19 -0
  243. package/server/pathGuard.ts +12 -0
  244. package/server/pathSafety.test.ts +38 -0
  245. package/server/pathSafety.ts +33 -0
  246. package/server/projectConfig.test.ts +67 -0
  247. package/server/projectConfig.ts +101 -0
  248. package/server/projectMode.test.ts +106 -0
  249. package/server/projectMode.ts +59 -0
  250. package/server/redact.test.ts +88 -0
  251. package/server/redact.ts +35 -0
  252. package/server/redactEgress.test.ts +281 -0
  253. package/server/runRegistry.ts +270 -0
  254. package/server/runsAuth.test.ts +119 -0
  255. package/server/runtime/RuntimeAdapter.ts +24 -0
  256. package/server/runtime/expressAdapter.test.ts +38 -0
  257. package/server/runtime/expressAdapter.ts +70 -0
  258. package/server/scenarioTest.test.ts +148 -0
  259. package/server/scenarioTest.ts +88 -0
  260. package/server/setupStatus.test.ts +127 -0
  261. package/server/subflowRunner.ts +104 -0
  262. package/server/testRunner.ts +216 -0
  263. package/server/tsconfig.json +20 -0
  264. package/server/workflowTestRoute.test.ts +297 -0
  265. package/src/App.tsx +233 -0
  266. package/src/components/AgentConsole.tsx +461 -0
  267. package/src/components/BranchRulesEditor.tsx +156 -0
  268. package/src/components/CreateModal.tsx +275 -0
  269. package/src/components/Dock.tsx +487 -0
  270. package/src/components/EnvironmentDialog.tsx +569 -0
  271. package/src/components/EnvironmentPicker.tsx +357 -0
  272. package/src/components/EnvironmentsDialog.tsx +144 -0
  273. package/src/components/ExecutionPager.tsx +67 -0
  274. package/src/components/InfraPanel.test.tsx +103 -0
  275. package/src/components/InfraPanel.tsx +171 -0
  276. package/src/components/Inspector.tsx +1182 -0
  277. package/src/components/Json.tsx +50 -0
  278. package/src/components/JsonModal.tsx +52 -0
  279. package/src/components/LogMessage.tsx +61 -0
  280. package/src/components/NodeRunModal.tsx +213 -0
  281. package/src/components/RunLogPanel.tsx +397 -0
  282. package/src/components/RunbookView.tsx +1085 -0
  283. package/src/components/Scenarios.tsx +476 -0
  284. package/src/components/SettingsDialog.tsx +229 -0
  285. package/src/components/Sidebar.tsx +436 -0
  286. package/src/components/StatusBar.tsx +302 -0
  287. package/src/components/Toolbar.tsx +663 -0
  288. package/src/components/WelcomeDialog.test.tsx +107 -0
  289. package/src/components/WelcomeDialog.tsx +412 -0
  290. package/src/components/agentMarkdown.test.tsx +91 -0
  291. package/src/components/agentMarkdown.tsx +158 -0
  292. package/src/components/ui/badge.tsx +31 -0
  293. package/src/components/ui/button.tsx +46 -0
  294. package/src/components/ui/combobox.tsx +114 -0
  295. package/src/components/ui/command.tsx +82 -0
  296. package/src/components/ui/dialog.tsx +73 -0
  297. package/src/components/ui/input.tsx +20 -0
  298. package/src/components/ui/popover.tsx +32 -0
  299. package/src/components/ui/resizable.tsx +44 -0
  300. package/src/components/ui/tabs.tsx +36 -0
  301. package/src/config.ts +4 -0
  302. package/src/engine/authVerify.ts +81 -0
  303. package/src/engine/diagnostics.test.ts +437 -0
  304. package/src/engine/diagnostics.ts +211 -0
  305. package/src/engine/executor.test.ts +1339 -0
  306. package/src/engine/executor.ts +840 -0
  307. package/src/engine/httpError.test.ts +12 -0
  308. package/src/engine/httpError.ts +16 -0
  309. package/src/engine/index.ts +11 -0
  310. package/src/engine/output.ts +21 -0
  311. package/src/engine/publish.test.ts +176 -0
  312. package/src/engine/publish.ts +127 -0
  313. package/src/engine/registry.test.ts +64 -0
  314. package/src/engine/registry.ts +80 -0
  315. package/src/engine/scenarios.test.ts +55 -0
  316. package/src/engine/scenarios.ts +29 -0
  317. package/src/engine/schemaCheck.test.ts +21 -0
  318. package/src/engine/schemaCheck.ts +35 -0
  319. package/src/engine/trace.test.ts +19 -0
  320. package/src/engine/trace.ts +23 -0
  321. package/src/engine/types.test.ts +22 -0
  322. package/src/engine/types.ts +316 -0
  323. package/src/engine/validation.test.ts +314 -0
  324. package/src/engine/validation.ts +354 -0
  325. package/src/flows/anomaly-flows.test.ts +81 -0
  326. package/src/flows/anomaly-flows.ts +202 -0
  327. package/src/flows/city-sweep-flow.ts +131 -0
  328. package/src/flows/flows.test.ts +30 -0
  329. package/src/flows/login-flow.ts +150 -0
  330. package/src/flows/pradar-flows.test.ts +204 -0
  331. package/src/flows/pradar-flows.ts +419 -0
  332. package/src/flows/weather-flow.ts +154 -0
  333. package/src/globals.css +147 -0
  334. package/src/lib/pathParams.test.ts +24 -0
  335. package/src/lib/pathParams.ts +6 -0
  336. package/src/lib/registerLens.test.ts +79 -0
  337. package/src/lib/registerLens.ts +59 -0
  338. package/src/lib/runbookModel.test.ts +61 -0
  339. package/src/lib/runbookModel.ts +290 -0
  340. package/src/lib/runbookProjection.test.ts +417 -0
  341. package/src/lib/runbookProjection.ts +254 -0
  342. package/src/lib/useTailAnchor.ts +53 -0
  343. package/src/lib/utils.ts +7 -0
  344. package/src/main.tsx +13 -0
  345. package/src/nodes/anomaly.test.ts +98 -0
  346. package/src/nodes/anomaly.ts +637 -0
  347. package/src/nodes/effects-scan.test.ts +44 -0
  348. package/src/nodes/flow-control.test.ts +280 -0
  349. package/src/nodes/flow-control.ts +305 -0
  350. package/src/nodes/index.ts +22 -0
  351. package/src/nodes/login.test.ts +81 -0
  352. package/src/nodes/login.ts +186 -0
  353. package/src/nodes/loops.ts +69 -0
  354. package/src/nodes/pradar/index.ts +11 -0
  355. package/src/nodes/pradar/logic.test.ts +288 -0
  356. package/src/nodes/pradar/logic.ts +582 -0
  357. package/src/nodes/pradar/nodes.test.ts +146 -0
  358. package/src/nodes/pradar/nodes.ts +955 -0
  359. package/src/nodes/requireAuth.test.ts +75 -0
  360. package/src/nodes/requireAuth.ts +55 -0
  361. package/src/nodes/response.test.ts +33 -0
  362. package/src/nodes/response.ts +39 -0
  363. package/src/nodes/traceDetail.test.ts +39 -0
  364. package/src/nodes/weather.test.ts +65 -0
  365. package/src/nodes/weather.ts +306 -0
  366. package/src/store/agentClient.test.ts +103 -0
  367. package/src/store/agentClient.ts +131 -0
  368. package/src/store/apiTree.test.ts +17 -0
  369. package/src/store/apiTree.ts +73 -0
  370. package/src/store/builderStore.test.ts +1234 -0
  371. package/src/store/builderStore.ts +1879 -0
  372. package/src/store/infraClient.ts +63 -0
  373. package/src/store/nodeMeta.test.ts +24 -0
  374. package/src/store/nodeMeta.ts +21 -0
  375. package/src/store/persistence.test.ts +15 -0
  376. package/src/store/persistence.ts +54 -0
  377. package/src/store/serverRunner.ts +381 -0
  378. package/src/store/setupClient.ts +57 -0
  379. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  380. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  381. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  382. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  383. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  384. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  385. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  386. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  387. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  388. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  389. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  390. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  391. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  392. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  393. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  394. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  395. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  396. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  397. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  398. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  399. package/studio-dist/assets/index-DNJwW-hM.css +1 -0
  400. package/studio-dist/assets/index-O26dKRjW.js +65 -0
  401. package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  402. package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  403. package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  404. package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  405. package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  406. package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  407. package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  408. package/studio-dist/favicon.svg +1 -0
  409. package/studio-dist/icons.svg +24 -0
  410. package/studio-dist/index.html +14 -0
  411. package/templates/skills/emberflow-basics/SKILL.md +316 -0
  412. package/templates/skills/emberflow-model-process/SKILL.md +209 -0
  413. package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
  414. package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
@@ -0,0 +1,126 @@
1
+ import type { AgentEvent } from './types';
2
+
3
+ interface CodexCommandExecutionItem {
4
+ type: 'command_execution';
5
+ command: string;
6
+ status?: 'in_progress' | 'completed' | 'failed';
7
+ exit_code?: number | null;
8
+ aggregated_output?: string;
9
+ }
10
+
11
+ interface CodexAgentMessageItem {
12
+ type: 'agent_message';
13
+ text: string;
14
+ }
15
+
16
+ interface CodexErrorItem {
17
+ type: 'error';
18
+ // Codex has shipped both field names for the human-readable detail.
19
+ text?: string;
20
+ message?: string;
21
+ }
22
+
23
+ /** An MCP tool call codex made (e.g. the Emberflow MCP server's run_operation).
24
+ * Field names are read defensively — codex's shape may nest server/tool under
25
+ * `invocation` or expose them flat, and the result may arrive as result/output. */
26
+ interface CodexMcpToolCallItem {
27
+ type: 'mcp_tool_call';
28
+ status?: 'in_progress' | 'completed' | 'failed';
29
+ server?: string;
30
+ tool?: string;
31
+ name?: string;
32
+ invocation?: { server?: string; tool?: string };
33
+ result?: unknown;
34
+ output?: unknown;
35
+ error?: string;
36
+ }
37
+
38
+ type CodexItem =
39
+ | CodexCommandExecutionItem
40
+ | CodexAgentMessageItem
41
+ | CodexErrorItem
42
+ | CodexMcpToolCallItem
43
+ | { type: string };
44
+
45
+ /** Map a codex mcp_tool_call item → an 'mcp' AgentEvent, tolerating shape drift. */
46
+ function mcpEventFrom(item: CodexMcpToolCallItem): AgentEvent {
47
+ const server = item.server ?? item.invocation?.server ?? 'mcp';
48
+ const tool = item.tool ?? item.name ?? item.invocation?.tool ?? 'tool';
49
+ const raw = item.error ?? item.result ?? item.output;
50
+ const output = raw === undefined ? undefined : typeof raw === 'string' ? raw : JSON.stringify(raw, null, 2);
51
+ return { type: 'mcp', mcpServer: server, mcpTool: tool, mcpStatus: item.status, ...(output !== undefined ? { output } : {}) };
52
+ }
53
+
54
+ interface CodexLine {
55
+ type: string;
56
+ item?: CodexItem;
57
+ usage?: Record<string, number>;
58
+ }
59
+
60
+ export function parseCodexLine(line: string): AgentEvent | null {
61
+ const trimmed = line.trim();
62
+ if (!trimmed) return null;
63
+
64
+ let parsed: CodexLine;
65
+ try {
66
+ parsed = JSON.parse(trimmed);
67
+ } catch {
68
+ return null;
69
+ }
70
+
71
+ switch (parsed.type) {
72
+ case 'thread.started':
73
+ case 'turn.started':
74
+ return { type: 'started' };
75
+
76
+ case 'item.started': {
77
+ const item = parsed.item;
78
+ if (item && item.type === 'command_execution') {
79
+ const commandItem = item as CodexCommandExecutionItem;
80
+ return {
81
+ type: 'command',
82
+ command: commandItem.command,
83
+ commandStatus: commandItem.status,
84
+ };
85
+ }
86
+ if (item && item.type === 'mcp_tool_call') return mcpEventFrom(item as CodexMcpToolCallItem);
87
+ return null;
88
+ }
89
+
90
+ case 'item.completed': {
91
+ const item = parsed.item;
92
+ if (!item) return null;
93
+ if (item.type === 'command_execution') {
94
+ const commandItem = item as CodexCommandExecutionItem;
95
+ return {
96
+ type: 'command',
97
+ command: commandItem.command,
98
+ exitCode: commandItem.exit_code,
99
+ output: commandItem.aggregated_output,
100
+ commandStatus: commandItem.status,
101
+ };
102
+ }
103
+ if (item.type === 'mcp_tool_call') return mcpEventFrom(item as CodexMcpToolCallItem);
104
+ if (item.type === 'agent_message') {
105
+ const messageItem = item as CodexAgentMessageItem;
106
+ return { type: 'message', text: messageItem.text };
107
+ }
108
+ if (item.type === 'error') {
109
+ // A codex `error` ITEM is a non-fatal diagnostic (e.g. an MCP server
110
+ // failing to auth) — NOT the run failing. Surface it as a message so it
111
+ // stays visible without terminating the run. A real failure is signalled
112
+ // by the process exiting non-zero with no `done` (see codexAdapter),
113
+ // which is the ONLY thing that yields a terminal AgentEvent 'error'.
114
+ const errorItem = item as CodexErrorItem;
115
+ return { type: 'message', text: `⚠ ${errorItem.text ?? errorItem.message ?? 'error'}` };
116
+ }
117
+ return null;
118
+ }
119
+
120
+ case 'turn.completed':
121
+ return { type: 'done', usage: parsed.usage };
122
+
123
+ default:
124
+ return null;
125
+ }
126
+ }
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { detectAgents, probe } from './detect';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const fixture = (name: string) => path.join(__dirname, '__fixtures__', name);
8
+
9
+ describe('detectAgents', () => {
10
+ it('returns only the CLIs the injected probe reports as present, with parsed versions', () => {
11
+ expect(detectAgents((bin) => (bin === 'codex' ? { version: '0.142.5' } : undefined))).toEqual([
12
+ { kind: 'codex', version: '0.142.5' },
13
+ ]);
14
+ expect(detectAgents((bin) => (bin === 'claude' ? { version: '1.2.3' } : undefined))).toEqual([
15
+ { kind: 'claude', version: '1.2.3' },
16
+ ]);
17
+ });
18
+
19
+ it('returns both when both are present', () => {
20
+ expect(detectAgents(() => ({ version: '1.0.0' }))).toEqual([
21
+ { kind: 'codex', version: '1.0.0' },
22
+ { kind: 'claude', version: '1.0.0' },
23
+ ]);
24
+ });
25
+
26
+ it('returns [] when neither is present', () => {
27
+ expect(detectAgents(() => undefined)).toEqual([]);
28
+ });
29
+
30
+ it('reports null version when present but the output has no parseable semver token', () => {
31
+ expect(detectAgents((bin) => (bin === 'codex' ? { version: null } : undefined))).toEqual([
32
+ { kind: 'codex', version: null },
33
+ ]);
34
+ });
35
+
36
+ it('defaults to a real PATH probe that returns an array', () => {
37
+ expect(Array.isArray(detectAgents())).toBe(true);
38
+ });
39
+ });
40
+
41
+ describe('probe (real spawnSync path)', () => {
42
+ it('parses the first semver-ish token out of well-formed --version output', () => {
43
+ expect(probe(fixture('fake-version-good.mjs'))).toEqual({ version: '0.142.5' });
44
+ });
45
+
46
+ it('returns a null version when the output is garbage with no semver token', () => {
47
+ expect(probe(fixture('fake-version-garbage.mjs'))).toEqual({ version: null });
48
+ });
49
+
50
+ it('returns a null version when the output is empty', () => {
51
+ expect(probe(fixture('fake-version-empty.mjs'))).toEqual({ version: null });
52
+ });
53
+
54
+ it('returns undefined when the binary does not exist', () => {
55
+ expect(probe('/nonexistent/nope-agent-cli')).toBeUndefined();
56
+ });
57
+
58
+ it('returns undefined when the binary exits nonzero', () => {
59
+ expect(probe(fixture('fake-version-fail.mjs'))).toBeUndefined();
60
+ });
61
+
62
+ it('returns undefined when the binary hangs past the timeout', () => {
63
+ expect(probe(fixture('fake-version-hang.mjs'))).toBeUndefined();
64
+ }, 10000);
65
+ });
@@ -0,0 +1,43 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import type { AgentKind } from './types';
3
+
4
+ const CANDIDATES: AgentKind[] = ['codex', 'claude'];
5
+
6
+ const VERSION_RE = /\d+\.\d+\.\d+/;
7
+
8
+ export interface DetectedAgent {
9
+ kind: AgentKind;
10
+ version: string | null;
11
+ }
12
+
13
+ /**
14
+ * Real PATH probe: runs `<bin> --version` and parses the first semver-ish
15
+ * token out of stdout. Returns `undefined` when the binary isn't present/
16
+ * runnable (spawn error, timeout, or nonzero exit); `null` version when it
17
+ * ran but produced no parseable version string.
18
+ */
19
+ export function probe(bin: string): { version: string | null } | undefined {
20
+ const result = spawnSync(bin, ['--version'], {
21
+ stdio: ['ignore', 'pipe', 'pipe'],
22
+ timeout: 3000,
23
+ encoding: 'utf8',
24
+ });
25
+ if (result.error !== undefined || result.status !== 0) return undefined;
26
+ const match = result.stdout?.match(VERSION_RE);
27
+ return { version: match ? match[0] : null };
28
+ }
29
+
30
+ /**
31
+ * Detects which coding-agent CLIs are available on PATH, and the version
32
+ * each reports (null when present but unparseable). `probeBin` is
33
+ * injectable so tests can stub PATH presence/version without shelling out
34
+ * for real binaries.
35
+ */
36
+ export function detectAgents(probeBin: (bin: string) => { version: string | null } | undefined = probe): DetectedAgent[] {
37
+ const found: DetectedAgent[] = [];
38
+ for (const kind of CANDIDATES) {
39
+ const result = probeBin(kind);
40
+ if (result) found.push({ kind, version: result.version });
41
+ }
42
+ return found;
43
+ }
@@ -0,0 +1,129 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { mkdtempSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+ import { changedFiles, diffSince, isGitRepo, revert, snapshot } from './gitScope';
7
+
8
+ function git(dir: string, args: string[]): string {
9
+ return execFileSync('git', args, { cwd: dir, encoding: 'utf8' });
10
+ }
11
+
12
+ describe('gitScope', () => {
13
+ let dir: string;
14
+
15
+ beforeEach(() => {
16
+ dir = mkdtempSync(join(tmpdir(), 'gitscope-'));
17
+ git(dir, ['init']);
18
+ git(dir, ['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '--allow-empty', '-m', 'init']);
19
+ writeFileSync(join(dir, 'tracked.txt'), 'original content\n');
20
+ git(dir, ['add', 'tracked.txt']);
21
+ git(dir, ['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'add tracked file']);
22
+ // pre-existing untracked file that must survive revert untouched
23
+ writeFileSync(join(dir, 'pre-existing-untracked.txt'), 'do not touch\n');
24
+ });
25
+
26
+ afterEach(() => {
27
+ rmSync(dir, { recursive: true, force: true });
28
+ });
29
+
30
+ it('isGitRepo returns true for a git repo and false otherwise', () => {
31
+ expect(isGitRepo(dir)).toBe(true);
32
+ const nonRepo = mkdtempSync(join(tmpdir(), 'not-a-repo-'));
33
+ try {
34
+ expect(isGitRepo(nonRepo)).toBe(false);
35
+ } finally {
36
+ rmSync(nonRepo, { recursive: true, force: true });
37
+ }
38
+ });
39
+
40
+ it('changedFiles lists a mutated tracked file and a newly-added file, diffSince shows the mutation', () => {
41
+ const snap = snapshot(dir);
42
+
43
+ writeFileSync(join(dir, 'tracked.txt'), 'mutated content\n');
44
+ writeFileSync(join(dir, 'new-file.txt'), 'brand new\n');
45
+
46
+ const files = changedFiles(dir, snap);
47
+ expect(files).toContain('tracked.txt');
48
+ expect(files).toContain('new-file.txt');
49
+ expect(files).not.toContain('pre-existing-untracked.txt');
50
+
51
+ const diff = diffSince(dir, snap);
52
+ expect(diff).toContain('mutated content');
53
+ });
54
+
55
+ it('revert restores the tracked file, removes the new file, and leaves pre-existing untracked files alone', () => {
56
+ const snap = snapshot(dir);
57
+
58
+ writeFileSync(join(dir, 'tracked.txt'), 'mutated content\n');
59
+ writeFileSync(join(dir, 'new-file.txt'), 'brand new\n');
60
+
61
+ const files = changedFiles(dir, snap);
62
+ revert(dir, snap, files);
63
+
64
+ expect(readFileSync(join(dir, 'tracked.txt'), 'utf8')).toBe('original content\n');
65
+ expect(existsSync(join(dir, 'new-file.txt'))).toBe(false);
66
+ expect(existsSync(join(dir, 'pre-existing-untracked.txt'))).toBe(true);
67
+ });
68
+
69
+ it('revert restores a file that was mutated AND staged (git add) since the snapshot', () => {
70
+ const snap = snapshot(dir);
71
+
72
+ writeFileSync(join(dir, 'tracked.txt'), 'mutated content\n');
73
+ git(dir, ['add', 'tracked.txt']);
74
+
75
+ const files = changedFiles(dir, snap);
76
+ expect(files).toContain('tracked.txt');
77
+
78
+ revert(dir, snap, files);
79
+
80
+ // Must be the snapshot content, not the staged (mutated) content.
81
+ expect(readFileSync(join(dir, 'tracked.txt'), 'utf8')).toBe('original content\n');
82
+ });
83
+
84
+ it('changedFiles/revert exclude a tracked file that was already dirty (WIP) at snapshot time', () => {
85
+ // Simulate the user having uncommitted WIP on tracked.txt *before* the agent runs.
86
+ writeFileSync(join(dir, 'tracked.txt'), 'user wip content\n');
87
+
88
+ const snap = snapshot(dir);
89
+ expect(snap.dirty).toContain('tracked.txt');
90
+
91
+ // Agent runs: creates a new file, does NOT touch tracked.txt.
92
+ writeFileSync(join(dir, 'new.txt'), 'agent created\n');
93
+
94
+ const files = changedFiles(dir, snap);
95
+ expect(files).toEqual(['new.txt']);
96
+ expect(files).not.toContain('tracked.txt');
97
+
98
+ revert(dir, snap, files);
99
+
100
+ // The user's WIP edit must survive untouched.
101
+ expect(readFileSync(join(dir, 'tracked.txt'), 'utf8')).toBe('user wip content\n');
102
+ // The agent's new file must be gone.
103
+ expect(existsSync(join(dir, 'new.txt'))).toBe(false);
104
+ });
105
+
106
+ it('snapshot / changedFiles / revert work on a fresh repo with no commits', () => {
107
+ const fresh = mkdtempSync(join(tmpdir(), 'gitscope-empty-'));
108
+ try {
109
+ git(fresh, ['init']);
110
+ writeFileSync(join(fresh, 'a.txt'), 'hello\n');
111
+
112
+ const snap = snapshot(fresh);
113
+ expect(snap.head).toBeNull();
114
+
115
+ // Add a new file after the snapshot.
116
+ writeFileSync(join(fresh, 'b.txt'), 'new\n');
117
+
118
+ const files = changedFiles(fresh, snap);
119
+ expect(files).toContain('b.txt');
120
+ expect(files).not.toContain('a.txt'); // pre-existing untracked, not a change
121
+
122
+ revert(fresh, snap, files);
123
+ expect(existsSync(join(fresh, 'b.txt'))).toBe(false);
124
+ expect(existsSync(join(fresh, 'a.txt'))).toBe(true); // left alone
125
+ } finally {
126
+ rmSync(fresh, { recursive: true, force: true });
127
+ }
128
+ });
129
+ });
@@ -0,0 +1,110 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ export interface GitSnapshot {
6
+ head: string | null;
7
+ dirty: string[];
8
+ }
9
+
10
+ function git(dir: string, args: string[]): string {
11
+ return execFileSync('git', args, { cwd: dir, encoding: 'utf8' });
12
+ }
13
+
14
+ /** Untracked (non-ignored) files, relative to `dir`. */
15
+ function untrackedFiles(dir: string): string[] {
16
+ const out = git(dir, ['ls-files', '--others', '--exclude-standard']);
17
+ return out.split('\n').filter((line) => line.length > 0);
18
+ }
19
+
20
+ /**
21
+ * Tracked files with uncommitted modifications (staged or unstaged), relative
22
+ * to `dir`. Diffs against the given commit; when `head` is null (a repo with no
23
+ * commits) there are no tracked-and-committed files, so returns [].
24
+ */
25
+ function dirtyTrackedFiles(dir: string, head: string | null): string[] {
26
+ if (!head) return [];
27
+ const out = git(dir, ['diff', '--name-only', head]);
28
+ return out.split('\n').filter((line) => line.length > 0);
29
+ }
30
+
31
+ export function isGitRepo(dir: string): boolean {
32
+ try {
33
+ const out = execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
34
+ cwd: dir,
35
+ encoding: 'utf8',
36
+ stdio: ['ignore', 'pipe', 'ignore'],
37
+ }).trim();
38
+ return out === 'true';
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ export function snapshot(dir: string): GitSnapshot {
45
+ let head: string | null = null;
46
+ try {
47
+ head = execFileSync('git', ['rev-parse', 'HEAD'], {
48
+ cwd: dir,
49
+ encoding: 'utf8',
50
+ stdio: ['ignore', 'pipe', 'ignore'],
51
+ }).trim();
52
+ } catch {
53
+ head = null;
54
+ }
55
+ const dirty = [...dirtyTrackedFiles(dir, head), ...untrackedFiles(dir)];
56
+ return { head, dirty };
57
+ }
58
+
59
+ export function diffSince(dir: string, snap: GitSnapshot): string {
60
+ if (snap.head) {
61
+ return git(dir, ['diff', snap.head]);
62
+ }
63
+ return git(dir, ['diff']);
64
+ }
65
+
66
+ export function changedFiles(dir: string, snap: GitSnapshot): string[] {
67
+ // Exclude any tracked file that was ALREADY dirty (uncommitted WIP) at
68
+ // snapshot time — mirrors the untracked filter below. We can't cleanly
69
+ // separate the user's pre-existing edits from the agent's own edits to the
70
+ // same file, so the safest choice is to leave such files out of
71
+ // `changedFiles` entirely: they won't be reported as agent changes, and
72
+ // (critically) `revert` won't touch them, preserving the user's WIP.
73
+ const trackedChanged = dirtyTrackedFiles(dir, snap.head).filter((f) => !snap.dirty.includes(f));
74
+
75
+ const newlyUntracked = untrackedFiles(dir).filter((f) => !snap.dirty.includes(f));
76
+
77
+ return [...new Set([...trackedChanged, ...newlyUntracked])];
78
+ }
79
+
80
+ /**
81
+ * Reverts the given files, which are expected to come from `changedFiles`
82
+ * (and therefore already exclude any untracked files that pre-date the
83
+ * snapshot).
84
+ *
85
+ * Tracked files are restored from the snapshot commit (`git checkout
86
+ * <snap.head> -- <file>`), NOT from the index — so a file that was `git add`ed
87
+ * after the snapshot is still restored to its snapshot content, not its staged
88
+ * (mutated) content. Files that are currently untracked (newly added since the
89
+ * snapshot) are deleted.
90
+ *
91
+ * If `snap.head` is null (a repo with no commits at snapshot time) there is no
92
+ * commit to restore tracked files from, so only the newly-added untracked files
93
+ * are deleted; any tracked-file changes are left in place.
94
+ */
95
+ export function revert(dir: string, snap: GitSnapshot, files: string[]): void {
96
+ if (files.length === 0) return;
97
+
98
+ const currentUntracked = new Set(untrackedFiles(dir));
99
+
100
+ const toDelete = files.filter((f) => currentUntracked.has(f));
101
+ const toCheckout = files.filter((f) => !currentUntracked.has(f));
102
+
103
+ for (const file of toDelete) {
104
+ rmSync(join(dir, file), { force: true });
105
+ }
106
+
107
+ if (toCheckout.length > 0 && snap.head) {
108
+ git(dir, ['checkout', snap.head, '--', ...toCheckout]);
109
+ }
110
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { modelRejectionHint } from './modelRejectionHint';
3
+
4
+ describe('modelRejectionHint', () => {
5
+ it('returns undefined for unrelated stderr', () => {
6
+ expect(modelRejectionHint('codex', 'some non-fatal mcp auth error')).toBeUndefined();
7
+ });
8
+
9
+ it('returns undefined for empty stderr', () => {
10
+ expect(modelRejectionHint('codex', '')).toBeUndefined();
11
+ });
12
+
13
+ it.each([
14
+ 'Error: unknown model "gpt-5.6-sol"',
15
+ "Error: unsupported model 'gpt-5.6-sol'",
16
+ 'invalid model specified',
17
+ 'model gpt-5.6-sol not found',
18
+ 'the requested model is not supported',
19
+ ])('matches known model-rejection shapes: %s', (stderr) => {
20
+ expect(modelRejectionHint('codex', stderr)).toBe(
21
+ 'hint: your codex CLI may be too old for the selected model — upgrade it or switch backend in Settings.',
22
+ );
23
+ });
24
+
25
+ it('names the given agent kind in the hint text', () => {
26
+ expect(modelRejectionHint('claude', 'unknown model')).toBe(
27
+ 'hint: your claude CLI may be too old for the selected model — upgrade it or switch backend in Settings.',
28
+ );
29
+ });
30
+ });
@@ -0,0 +1,14 @@
1
+ import type { AgentKind } from './types';
2
+
3
+ const MODEL_REJECTION_RE = /unknown model|unsupported model|invalid model|model.*not.*(found|supported)/i;
4
+
5
+ /**
6
+ * When an agent CLI exits nonzero without ever emitting a terminal `done`,
7
+ * inspect its buffered stderr tail for known model-rejection shapes (a stale
8
+ * CLI that doesn't recognize the requested model). Returns an actionable
9
+ * hint to append to the error event text, or undefined when nothing matches.
10
+ */
11
+ export function modelRejectionHint(kind: AgentKind, stderrTail: string): string | undefined {
12
+ if (!MODEL_REJECTION_RE.test(stderrTail)) return undefined;
13
+ return `hint: your ${kind} CLI may be too old for the selected model — upgrade it or switch backend in Settings.`;
14
+ }