@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.
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/commands.test.ts +185 -0
- package/bin/commands.ts +306 -0
- package/bin/emberflow.mjs +38 -0
- package/bin/init.test.ts +165 -0
- package/bin/init.ts +297 -0
- package/bin/runtime.mjs +105 -0
- package/bin/runtime.test.ts +167 -0
- package/bin/skillTargets.test.ts +108 -0
- package/bin/skillTargets.ts +36 -0
- package/bin/skillTemplates.test.ts +37 -0
- package/bin/tsconfig.json +23 -0
- package/dist/bin/commands.d.ts +24 -0
- package/dist/bin/commands.js +252 -0
- package/dist/bin/init.d.ts +17 -0
- package/dist/bin/init.js +241 -0
- package/dist/bin/runtime.d.mts +39 -0
- package/dist/bin/runtime.mjs +96 -0
- package/dist/bin/skillTargets.d.ts +13 -0
- package/dist/bin/skillTargets.js +22 -0
- package/dist/server/agents/claudeAdapter.d.ts +8 -0
- package/dist/server/agents/claudeAdapter.js +190 -0
- package/dist/server/agents/claudeParse.d.ts +2 -0
- package/dist/server/agents/claudeParse.js +162 -0
- package/dist/server/agents/codexAdapter.d.ts +13 -0
- package/dist/server/agents/codexAdapter.js +174 -0
- package/dist/server/agents/codexParse.d.ts +2 -0
- package/dist/server/agents/codexParse.js +74 -0
- package/dist/server/agents/detect.d.ts +23 -0
- package/dist/server/agents/detect.js +35 -0
- package/dist/server/agents/gitScope.d.ts +24 -0
- package/dist/server/agents/gitScope.js +95 -0
- package/dist/server/agents/modelRejectionHint.d.ts +8 -0
- package/dist/server/agents/modelRejectionHint.js +12 -0
- package/dist/server/agents/prompt.d.ts +79 -0
- package/dist/server/agents/prompt.js +364 -0
- package/dist/server/agents/runManager.d.ts +90 -0
- package/dist/server/agents/runManager.js +259 -0
- package/dist/server/agents/types.d.ts +15 -0
- package/dist/server/agents/types.js +1 -0
- package/dist/server/apiStore.d.ts +60 -0
- package/dist/server/apiStore.js +249 -0
- package/dist/server/auth/enforce.d.ts +1 -0
- package/dist/server/auth/enforce.js +6 -0
- package/dist/server/auth/types.d.ts +1 -0
- package/dist/server/auth/types.js +1 -0
- package/dist/server/auth/verifiers.d.ts +1 -0
- package/dist/server/auth/verifiers.js +4 -0
- package/dist/server/authAttach.d.ts +19 -0
- package/dist/server/authAttach.js +79 -0
- package/dist/server/cli.d.ts +7 -0
- package/dist/server/cli.js +804 -0
- package/dist/server/client.d.ts +130 -0
- package/dist/server/client.js +247 -0
- package/dist/server/environments.d.ts +103 -0
- package/dist/server/environments.js +486 -0
- package/dist/server/flowStore.d.ts +37 -0
- package/dist/server/flowStore.js +117 -0
- package/dist/server/httpOperations.d.ts +32 -0
- package/dist/server/httpOperations.js +62 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1022 -0
- package/dist/server/infrastructure.d.ts +51 -0
- package/dist/server/infrastructure.js +116 -0
- package/dist/server/login.d.ts +21 -0
- package/dist/server/login.js +44 -0
- package/dist/server/mcp.d.ts +1 -0
- package/dist/server/mcp.js +182 -0
- package/dist/server/migrateFlows.d.ts +8 -0
- package/dist/server/migrateFlows.js +25 -0
- package/dist/server/mockHandler.d.ts +15 -0
- package/dist/server/mockHandler.js +35 -0
- package/dist/server/nodesPayload.d.ts +14 -0
- package/dist/server/nodesPayload.js +14 -0
- package/dist/server/normalizeFlow.d.ts +21 -0
- package/dist/server/normalizeFlow.js +48 -0
- package/dist/server/openBrowser.d.ts +8 -0
- package/dist/server/openBrowser.js +16 -0
- package/dist/server/operationResult.d.ts +12 -0
- package/dist/server/operationResult.js +26 -0
- package/dist/server/pathGuard.d.ts +1 -0
- package/dist/server/pathGuard.js +16 -0
- package/dist/server/pathSafety.d.ts +20 -0
- package/dist/server/pathSafety.js +35 -0
- package/dist/server/projectConfig.d.ts +47 -0
- package/dist/server/projectConfig.js +61 -0
- package/dist/server/projectMode.d.ts +26 -0
- package/dist/server/projectMode.js +44 -0
- package/dist/server/redact.d.ts +6 -0
- package/dist/server/redact.js +37 -0
- package/dist/server/runRegistry.d.ts +84 -0
- package/dist/server/runRegistry.js +200 -0
- package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
- package/dist/server/runtime/RuntimeAdapter.js +1 -0
- package/dist/server/runtime/expressAdapter.d.ts +19 -0
- package/dist/server/runtime/expressAdapter.js +56 -0
- package/dist/server/scenarioTest.d.ts +14 -0
- package/dist/server/scenarioTest.js +66 -0
- package/dist/server/subflowRunner.d.ts +32 -0
- package/dist/server/subflowRunner.js +72 -0
- package/dist/server/testRunner.d.ts +70 -0
- package/dist/server/testRunner.js +151 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +2 -0
- package/dist/src/engine/authVerify.d.ts +33 -0
- package/dist/src/engine/authVerify.js +63 -0
- package/dist/src/engine/diagnostics.d.ts +62 -0
- package/dist/src/engine/diagnostics.js +148 -0
- package/dist/src/engine/executor.d.ts +151 -0
- package/dist/src/engine/executor.js +684 -0
- package/dist/src/engine/httpError.d.ts +7 -0
- package/dist/src/engine/httpError.js +14 -0
- package/dist/src/engine/index.d.ts +11 -0
- package/dist/src/engine/index.js +11 -0
- package/dist/src/engine/output.d.ts +11 -0
- package/dist/src/engine/output.js +20 -0
- package/dist/src/engine/publish.d.ts +30 -0
- package/dist/src/engine/publish.js +95 -0
- package/dist/src/engine/registry.d.ts +34 -0
- package/dist/src/engine/registry.js +67 -0
- package/dist/src/engine/scenarios.d.ts +12 -0
- package/dist/src/engine/scenarios.js +19 -0
- package/dist/src/engine/schemaCheck.d.ts +4 -0
- package/dist/src/engine/schemaCheck.js +34 -0
- package/dist/src/engine/trace.d.ts +12 -0
- package/dist/src/engine/trace.js +12 -0
- package/dist/src/engine/types.d.ts +308 -0
- package/dist/src/engine/types.js +1 -0
- package/dist/src/engine/validation.d.ts +26 -0
- package/dist/src/engine/validation.js +318 -0
- package/dist/src/flows/anomaly-flows.d.ts +23 -0
- package/dist/src/flows/anomaly-flows.js +151 -0
- package/dist/src/flows/city-sweep-flow.d.ts +10 -0
- package/dist/src/flows/city-sweep-flow.js +127 -0
- package/dist/src/flows/login-flow.d.ts +13 -0
- package/dist/src/flows/login-flow.js +145 -0
- package/dist/src/flows/pradar-flows.d.ts +5 -0
- package/dist/src/flows/pradar-flows.js +375 -0
- package/dist/src/flows/weather-flow.d.ts +12 -0
- package/dist/src/flows/weather-flow.js +150 -0
- package/dist/src/lib/pathParams.d.ts +3 -0
- package/dist/src/lib/pathParams.js +6 -0
- package/dist/src/nodes/anomaly.d.ts +2 -0
- package/dist/src/nodes/anomaly.js +556 -0
- package/dist/src/nodes/flow-control.d.ts +7 -0
- package/dist/src/nodes/flow-control.js +270 -0
- package/dist/src/nodes/index.d.ts +3 -0
- package/dist/src/nodes/index.js +20 -0
- package/dist/src/nodes/login.d.ts +7 -0
- package/dist/src/nodes/login.js +158 -0
- package/dist/src/nodes/loops.d.ts +11 -0
- package/dist/src/nodes/loops.js +60 -0
- package/dist/src/nodes/pradar/index.d.ts +7 -0
- package/dist/src/nodes/pradar/index.js +9 -0
- package/dist/src/nodes/pradar/logic.d.ts +189 -0
- package/dist/src/nodes/pradar/logic.js +370 -0
- package/dist/src/nodes/pradar/nodes.d.ts +42 -0
- package/dist/src/nodes/pradar/nodes.js +801 -0
- package/dist/src/nodes/requireAuth.d.ts +20 -0
- package/dist/src/nodes/requireAuth.js +48 -0
- package/dist/src/nodes/response.d.ts +8 -0
- package/dist/src/nodes/response.js +33 -0
- package/dist/src/nodes/weather.d.ts +33 -0
- package/dist/src/nodes/weather.js +260 -0
- package/package.json +92 -0
- package/server/agentRoute.test.ts +157 -0
- package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
- package/server/agents/__fixtures__/fake-claude.mjs +55 -0
- package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
- package/server/agents/__fixtures__/fake-codex.mjs +52 -0
- package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
- package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
- package/server/agents/claudeAdapter.test.ts +127 -0
- package/server/agents/claudeAdapter.ts +212 -0
- package/server/agents/claudeParse.ts +193 -0
- package/server/agents/codexAdapter.test.ts +125 -0
- package/server/agents/codexAdapter.ts +198 -0
- package/server/agents/codexParse.test.ts +58 -0
- package/server/agents/codexParse.ts +126 -0
- package/server/agents/detect.test.ts +65 -0
- package/server/agents/detect.ts +43 -0
- package/server/agents/gitScope.test.ts +129 -0
- package/server/agents/gitScope.ts +110 -0
- package/server/agents/modelRejectionHint.test.ts +30 -0
- package/server/agents/modelRejectionHint.ts +14 -0
- package/server/agents/prompt.test.ts +613 -0
- package/server/agents/prompt.ts +527 -0
- package/server/agents/runManager.test.ts +251 -0
- package/server/agents/runManager.ts +286 -0
- package/server/agents/types.ts +17 -0
- package/server/apiMount.test.ts +591 -0
- package/server/apiStore.test.ts +324 -0
- package/server/apiStore.ts +241 -0
- package/server/auth/enforce.test.ts +61 -0
- package/server/auth/enforce.ts +6 -0
- package/server/auth/types.ts +5 -0
- package/server/auth/verifiers.test.ts +37 -0
- package/server/auth/verifiers.ts +9 -0
- package/server/authAttach.test.ts +175 -0
- package/server/authAttach.ts +99 -0
- package/server/cli.ts +847 -0
- package/server/cliAuth.test.ts +229 -0
- package/server/cliCreate.test.ts +131 -0
- package/server/cliDoctor.test.ts +326 -0
- package/server/cliTest.test.ts +735 -0
- package/server/client.ts +314 -0
- package/server/demoProject.test.ts +29 -0
- package/server/diagnosticsRoute.test.ts +169 -0
- package/server/environments.test.ts +620 -0
- package/server/environments.ts +603 -0
- package/server/environmentsRoute.test.ts +385 -0
- package/server/errorWorkflow.test.ts +205 -0
- package/server/flowStore.test.ts +84 -0
- package/server/flowStore.ts +125 -0
- package/server/httpOperations.test.ts +377 -0
- package/server/httpOperations.ts +96 -0
- package/server/index.ts +1111 -0
- package/server/infrastructure.test.ts +154 -0
- package/server/infrastructure.ts +161 -0
- package/server/infrastructureRoute.test.ts +105 -0
- package/server/login.test.ts +153 -0
- package/server/login.ts +67 -0
- package/server/mcp.ts +278 -0
- package/server/migrateFlows.test.ts +55 -0
- package/server/migrateFlows.ts +23 -0
- package/server/mockHandler.test.ts +138 -0
- package/server/mockHandler.ts +52 -0
- package/server/mockRun.test.ts +367 -0
- package/server/mockServe.test.ts +467 -0
- package/server/nodesPayload.test.ts +29 -0
- package/server/nodesPayload.ts +26 -0
- package/server/normalizeFlow.test.ts +80 -0
- package/server/normalizeFlow.ts +59 -0
- package/server/openBrowser.test.ts +20 -0
- package/server/openBrowser.ts +22 -0
- package/server/operationResult.test.ts +67 -0
- package/server/operationResult.ts +27 -0
- package/server/pathGuard.test.ts +19 -0
- package/server/pathGuard.ts +12 -0
- package/server/pathSafety.test.ts +38 -0
- package/server/pathSafety.ts +33 -0
- package/server/projectConfig.test.ts +67 -0
- package/server/projectConfig.ts +101 -0
- package/server/projectMode.test.ts +106 -0
- package/server/projectMode.ts +59 -0
- package/server/redact.test.ts +88 -0
- package/server/redact.ts +35 -0
- package/server/redactEgress.test.ts +281 -0
- package/server/runRegistry.ts +270 -0
- package/server/runsAuth.test.ts +119 -0
- package/server/runtime/RuntimeAdapter.ts +24 -0
- package/server/runtime/expressAdapter.test.ts +38 -0
- package/server/runtime/expressAdapter.ts +70 -0
- package/server/scenarioTest.test.ts +148 -0
- package/server/scenarioTest.ts +88 -0
- package/server/setupStatus.test.ts +127 -0
- package/server/subflowRunner.ts +104 -0
- package/server/testRunner.ts +216 -0
- package/server/tsconfig.json +20 -0
- package/server/workflowTestRoute.test.ts +297 -0
- package/src/App.tsx +233 -0
- package/src/components/AgentConsole.tsx +461 -0
- package/src/components/BranchRulesEditor.tsx +156 -0
- package/src/components/CreateModal.tsx +275 -0
- package/src/components/Dock.tsx +487 -0
- package/src/components/EnvironmentDialog.tsx +569 -0
- package/src/components/EnvironmentPicker.tsx +357 -0
- package/src/components/EnvironmentsDialog.tsx +144 -0
- package/src/components/ExecutionPager.tsx +67 -0
- package/src/components/InfraPanel.test.tsx +103 -0
- package/src/components/InfraPanel.tsx +171 -0
- package/src/components/Inspector.tsx +1182 -0
- package/src/components/Json.tsx +50 -0
- package/src/components/JsonModal.tsx +52 -0
- package/src/components/LogMessage.tsx +61 -0
- package/src/components/NodeRunModal.tsx +213 -0
- package/src/components/RunLogPanel.tsx +397 -0
- package/src/components/RunbookView.tsx +1085 -0
- package/src/components/Scenarios.tsx +476 -0
- package/src/components/SettingsDialog.tsx +229 -0
- package/src/components/Sidebar.tsx +436 -0
- package/src/components/StatusBar.tsx +302 -0
- package/src/components/Toolbar.tsx +663 -0
- package/src/components/WelcomeDialog.test.tsx +107 -0
- package/src/components/WelcomeDialog.tsx +412 -0
- package/src/components/agentMarkdown.test.tsx +91 -0
- package/src/components/agentMarkdown.tsx +158 -0
- package/src/components/ui/badge.tsx +31 -0
- package/src/components/ui/button.tsx +46 -0
- package/src/components/ui/combobox.tsx +114 -0
- package/src/components/ui/command.tsx +82 -0
- package/src/components/ui/dialog.tsx +73 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/popover.tsx +32 -0
- package/src/components/ui/resizable.tsx +44 -0
- package/src/components/ui/tabs.tsx +36 -0
- package/src/config.ts +4 -0
- package/src/engine/authVerify.ts +81 -0
- package/src/engine/diagnostics.test.ts +437 -0
- package/src/engine/diagnostics.ts +211 -0
- package/src/engine/executor.test.ts +1339 -0
- package/src/engine/executor.ts +840 -0
- package/src/engine/httpError.test.ts +12 -0
- package/src/engine/httpError.ts +16 -0
- package/src/engine/index.ts +11 -0
- package/src/engine/output.ts +21 -0
- package/src/engine/publish.test.ts +176 -0
- package/src/engine/publish.ts +127 -0
- package/src/engine/registry.test.ts +64 -0
- package/src/engine/registry.ts +80 -0
- package/src/engine/scenarios.test.ts +55 -0
- package/src/engine/scenarios.ts +29 -0
- package/src/engine/schemaCheck.test.ts +21 -0
- package/src/engine/schemaCheck.ts +35 -0
- package/src/engine/trace.test.ts +19 -0
- package/src/engine/trace.ts +23 -0
- package/src/engine/types.test.ts +22 -0
- package/src/engine/types.ts +316 -0
- package/src/engine/validation.test.ts +314 -0
- package/src/engine/validation.ts +354 -0
- package/src/flows/anomaly-flows.test.ts +81 -0
- package/src/flows/anomaly-flows.ts +202 -0
- package/src/flows/city-sweep-flow.ts +131 -0
- package/src/flows/flows.test.ts +30 -0
- package/src/flows/login-flow.ts +150 -0
- package/src/flows/pradar-flows.test.ts +204 -0
- package/src/flows/pradar-flows.ts +419 -0
- package/src/flows/weather-flow.ts +154 -0
- package/src/globals.css +147 -0
- package/src/lib/pathParams.test.ts +24 -0
- package/src/lib/pathParams.ts +6 -0
- package/src/lib/registerLens.test.ts +79 -0
- package/src/lib/registerLens.ts +59 -0
- package/src/lib/runbookModel.test.ts +61 -0
- package/src/lib/runbookModel.ts +290 -0
- package/src/lib/runbookProjection.test.ts +417 -0
- package/src/lib/runbookProjection.ts +254 -0
- package/src/lib/useTailAnchor.ts +53 -0
- package/src/lib/utils.ts +7 -0
- package/src/main.tsx +13 -0
- package/src/nodes/anomaly.test.ts +98 -0
- package/src/nodes/anomaly.ts +637 -0
- package/src/nodes/effects-scan.test.ts +44 -0
- package/src/nodes/flow-control.test.ts +280 -0
- package/src/nodes/flow-control.ts +305 -0
- package/src/nodes/index.ts +22 -0
- package/src/nodes/login.test.ts +81 -0
- package/src/nodes/login.ts +186 -0
- package/src/nodes/loops.ts +69 -0
- package/src/nodes/pradar/index.ts +11 -0
- package/src/nodes/pradar/logic.test.ts +288 -0
- package/src/nodes/pradar/logic.ts +582 -0
- package/src/nodes/pradar/nodes.test.ts +146 -0
- package/src/nodes/pradar/nodes.ts +955 -0
- package/src/nodes/requireAuth.test.ts +75 -0
- package/src/nodes/requireAuth.ts +55 -0
- package/src/nodes/response.test.ts +33 -0
- package/src/nodes/response.ts +39 -0
- package/src/nodes/traceDetail.test.ts +39 -0
- package/src/nodes/weather.test.ts +65 -0
- package/src/nodes/weather.ts +306 -0
- package/src/store/agentClient.test.ts +103 -0
- package/src/store/agentClient.ts +131 -0
- package/src/store/apiTree.test.ts +17 -0
- package/src/store/apiTree.ts +73 -0
- package/src/store/builderStore.test.ts +1234 -0
- package/src/store/builderStore.ts +1879 -0
- package/src/store/infraClient.ts +63 -0
- package/src/store/nodeMeta.test.ts +24 -0
- package/src/store/nodeMeta.ts +21 -0
- package/src/store/persistence.test.ts +15 -0
- package/src/store/persistence.ts +54 -0
- package/src/store/serverRunner.ts +381 -0
- package/src/store/setupClient.ts +57 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/studio-dist/assets/index-DNJwW-hM.css +1 -0
- package/studio-dist/assets/index-O26dKRjW.js +65 -0
- package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/studio-dist/favicon.svg +1 -0
- package/studio-dist/icons.svg +24 -0
- package/studio-dist/index.html +14 -0
- package/templates/skills/emberflow-basics/SKILL.md +316 -0
- package/templates/skills/emberflow-model-process/SKILL.md +209 -0
- package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
- package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
7
|
+
import { AgentRunManager } from './runManager';
|
|
8
|
+
import type { AgentEvent } from './types';
|
|
9
|
+
import type { AgentIntent } from './prompt';
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const fakeCodexWriterPath = join(__dirname, '__fixtures__', 'fake-codex-writer.mjs');
|
|
13
|
+
const fakeClaudeWriterPath = join(__dirname, '__fixtures__', 'fake-claude-writer.mjs');
|
|
14
|
+
|
|
15
|
+
function git(dir: string, args: string[]): string {
|
|
16
|
+
return execFileSync('git', args, { cwd: dir, encoding: 'utf8' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function collectUntilDone(
|
|
20
|
+
manager: AgentRunManager,
|
|
21
|
+
id: string,
|
|
22
|
+
timeoutMs = 5000,
|
|
23
|
+
): Promise<AgentEvent[]> {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const events: AgentEvent[] = [];
|
|
26
|
+
const timer = setTimeout(() => reject(new Error('timed out waiting for done')), timeoutMs);
|
|
27
|
+
const unsubscribe = manager.subscribe(id, (event) => {
|
|
28
|
+
events.push(event);
|
|
29
|
+
if (event.type === 'done' || event.type === 'error') {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
unsubscribe?.();
|
|
32
|
+
resolve(events);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('AgentRunManager', () => {
|
|
39
|
+
let projectDir: string;
|
|
40
|
+
let flowsDir: string;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
projectDir = mkdtempSync(join(tmpdir(), 'agent-run-manager-'));
|
|
44
|
+
git(projectDir, ['init']);
|
|
45
|
+
git(projectDir, ['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '--allow-empty', '-m', 'init']);
|
|
46
|
+
|
|
47
|
+
flowsDir = join(projectDir, 'flows');
|
|
48
|
+
mkdirSync(flowsDir, { recursive: true });
|
|
49
|
+
writeFileSync(join(flowsDir, 'hello.json'), JSON.stringify({ id: 'hello', name: 'Hello', nodes: [] }));
|
|
50
|
+
git(projectDir, ['add', '-A']);
|
|
51
|
+
git(projectDir, ['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'add flow']);
|
|
52
|
+
|
|
53
|
+
process.env.EMBERFLOW_CODEX_BIN = fakeCodexWriterPath;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
delete process.env.EMBERFLOW_CODEX_BIN;
|
|
58
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const intent: AgentIntent = { action: 'edit-flow', flowId: 'hello', instruction: 'do a thing' };
|
|
62
|
+
const pathOf = (id: string): string | undefined => (id === 'hello' ? 'hello' : undefined);
|
|
63
|
+
|
|
64
|
+
it('starts a run, streams events to done, diffs the real change, and reverts it', async () => {
|
|
65
|
+
const manager = new AgentRunManager(projectDir, flowsDir, pathOf);
|
|
66
|
+
|
|
67
|
+
const id = manager.start(intent);
|
|
68
|
+
expect(typeof id).toBe('string');
|
|
69
|
+
expect(id.length).toBeGreaterThan(0);
|
|
70
|
+
|
|
71
|
+
const events = await collectUntilDone(manager, id);
|
|
72
|
+
expect(events[0]).toEqual({ type: 'started' });
|
|
73
|
+
expect(events[events.length - 1].type).toBe('done');
|
|
74
|
+
|
|
75
|
+
const { diff, files } = manager.diff(id)!;
|
|
76
|
+
expect(files).toContain('flows/hello.json');
|
|
77
|
+
expect(diff).toContain('agent was here');
|
|
78
|
+
expect(readFileSync(join(projectDir, 'flows', 'hello.json'), 'utf8')).toBe('agent was here\n');
|
|
79
|
+
|
|
80
|
+
const { reverted } = manager.revert(id)!;
|
|
81
|
+
expect(reverted).toContain('flows/hello.json');
|
|
82
|
+
expect(readFileSync(join(projectDir, 'flows', 'hello.json'), 'utf8')).not.toBe('agent was here\n');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('rejects a second concurrent start while a run is active', async () => {
|
|
86
|
+
const manager = new AgentRunManager(projectDir, flowsDir, pathOf);
|
|
87
|
+
|
|
88
|
+
// Use a slow-ish fake so the first run is still "active" when we try the second.
|
|
89
|
+
// fake-codex-writer exits synchronously fast, so start a run and immediately
|
|
90
|
+
// try again before draining events — the manager must still consider it busy
|
|
91
|
+
// until the run reaches a terminal state.
|
|
92
|
+
manager.start(intent);
|
|
93
|
+
expect(() => manager.start(intent)).toThrow(/busy|active/i);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('rejects starting against a non-git project dir', () => {
|
|
97
|
+
const nonRepo = mkdtempSync(join(tmpdir(), 'agent-run-manager-nongit-'));
|
|
98
|
+
try {
|
|
99
|
+
const manager = new AgentRunManager(nonRepo, flowsDir, pathOf);
|
|
100
|
+
expect(() => manager.start(intent)).toThrow(/not a git repo/i);
|
|
101
|
+
} finally {
|
|
102
|
+
rmSync(nonRepo, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('rejects starting for an unknown flow', () => {
|
|
107
|
+
const manager = new AgentRunManager(projectDir, flowsDir, pathOf);
|
|
108
|
+
const badIntent: AgentIntent = { action: 'edit-flow', flowId: 'nope', instruction: 'x' };
|
|
109
|
+
expect(() => manager.start(badIntent)).toThrow(/unknown flow/i);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('allows a new run once the previous one has finished', async () => {
|
|
113
|
+
const manager = new AgentRunManager(projectDir, flowsDir, pathOf);
|
|
114
|
+
const id1 = manager.start(intent);
|
|
115
|
+
await collectUntilDone(manager, id1);
|
|
116
|
+
manager.revert(id1);
|
|
117
|
+
|
|
118
|
+
const id2 = manager.start(intent);
|
|
119
|
+
expect(id2).not.toBe(id1);
|
|
120
|
+
await collectUntilDone(manager, id2);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('cancel calls through to the adapter without throwing', async () => {
|
|
124
|
+
const manager = new AgentRunManager(projectDir, flowsDir, pathOf);
|
|
125
|
+
const id = manager.start(intent);
|
|
126
|
+
expect(() => manager.cancel(id)).not.toThrow();
|
|
127
|
+
await collectUntilDone(manager, id);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('rejects a flowId that tries to escape flowsDir', () => {
|
|
131
|
+
const manager = new AgentRunManager(projectDir, flowsDir, pathOf);
|
|
132
|
+
const badIntent: AgentIntent = { action: 'edit-flow', flowId: '../../etc/passwd', instruction: 'x' };
|
|
133
|
+
expect(() => manager.start(badIntent)).toThrow(/invalid flowId/i);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('rejects a flowId with a ".." segment even when mixed with safe segments', () => {
|
|
137
|
+
const manager = new AgentRunManager(projectDir, flowsDir, pathOf);
|
|
138
|
+
const badIntent: AgentIntent = { action: 'edit-flow', flowId: 'a/../b', instruction: 'x' };
|
|
139
|
+
expect(() => manager.start(badIntent)).toThrow(/invalid flowId/i);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('accepts an availableNodes getter and still starts/completes a run normally', async () => {
|
|
143
|
+
const availableNodes = (): Array<{ type: string; label?: string; description?: string }> => [
|
|
144
|
+
{ type: 'Input' },
|
|
145
|
+
{ type: 'Response' },
|
|
146
|
+
{ type: 'FetchUser', label: 'Fetch User', description: 'Looks up a user by id.' },
|
|
147
|
+
];
|
|
148
|
+
const manager = new AgentRunManager(projectDir, flowsDir, pathOf, availableNodes);
|
|
149
|
+
|
|
150
|
+
const id = manager.start(intent);
|
|
151
|
+
const events = await collectUntilDone(manager, id);
|
|
152
|
+
expect(events[events.length - 1].type).toBe('done');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('allows a slash-separated path-style flowId (operation ids are on-disk paths) to pass the id-safety check', () => {
|
|
156
|
+
// billing/charge is a legitimate nested operation id — pathOf resolves it,
|
|
157
|
+
// so start() must get past the id-safety check and fail later for an
|
|
158
|
+
// unrelated reason (no such file), not with "invalid flowId".
|
|
159
|
+
const nestedPathOf = (id: string): string | undefined => (id === 'billing/charge' ? 'billing/charge' : undefined);
|
|
160
|
+
const manager = new AgentRunManager(projectDir, flowsDir, nestedPathOf);
|
|
161
|
+
const nestedIntent: AgentIntent = { action: 'edit-flow', flowId: 'billing/charge', instruction: 'x' };
|
|
162
|
+
expect(() => manager.start(nestedIntent)).not.toThrow(/invalid flowId/i);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('prunes old terminal runs beyond the cap while keeping the active run', async () => {
|
|
166
|
+
const manager = new AgentRunManager(projectDir, flowsDir, pathOf);
|
|
167
|
+
|
|
168
|
+
const completedIds: string[] = [];
|
|
169
|
+
for (let i = 0; i < 25; i++) {
|
|
170
|
+
const id = manager.start(intent);
|
|
171
|
+
await collectUntilDone(manager, id);
|
|
172
|
+
completedIds.push(id);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// The manager only prunes on the *next* start(), so start one more run
|
|
176
|
+
// to trigger the sweep, then inspect via `has()` since there's no direct
|
|
177
|
+
// size getter.
|
|
178
|
+
const activeId = manager.start(intent);
|
|
179
|
+
const remaining = [...completedIds, activeId].filter((id) => manager.has(id));
|
|
180
|
+
// 20 terminal runs kept + the newly-started active run.
|
|
181
|
+
expect(remaining.length).toBeLessThanOrEqual(21);
|
|
182
|
+
expect(manager.has(activeId)).toBe(true);
|
|
183
|
+
await collectUntilDone(manager, activeId);
|
|
184
|
+
}, 20000);
|
|
185
|
+
|
|
186
|
+
it('starts a new-operation run for a valid location without the unknown-flow check', async () => {
|
|
187
|
+
const manager = new AgentRunManager(projectDir, flowsDir, pathOf);
|
|
188
|
+
const newOpIntent: AgentIntent = {
|
|
189
|
+
action: 'new-operation',
|
|
190
|
+
location: 'billing',
|
|
191
|
+
instruction: 'Let a customer request a refund.',
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const id = manager.start(newOpIntent);
|
|
195
|
+
expect(typeof id).toBe('string');
|
|
196
|
+
|
|
197
|
+
const events = await collectUntilDone(manager, id);
|
|
198
|
+
expect(events[0]).toEqual({ type: 'started' });
|
|
199
|
+
expect(events[events.length - 1].type).toBe('done');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('rejects a new-operation with a location that tries to escape apisDir', () => {
|
|
203
|
+
const manager = new AgentRunManager(projectDir, flowsDir, pathOf);
|
|
204
|
+
const badIntent: AgentIntent = {
|
|
205
|
+
action: 'new-operation',
|
|
206
|
+
location: '../../etc',
|
|
207
|
+
instruction: 'x',
|
|
208
|
+
};
|
|
209
|
+
expect(() => manager.start(badIntent)).toThrow(/invalid location/i);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('starts a cover-operation run for a known flowId, resolving relPath like edit-flow', async () => {
|
|
213
|
+
const manager = new AgentRunManager(projectDir, flowsDir, pathOf);
|
|
214
|
+
const coverIntent: AgentIntent = {
|
|
215
|
+
action: 'cover-operation',
|
|
216
|
+
flowId: 'hello',
|
|
217
|
+
instruction: 'Cover every branch with scenarios.',
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const id = manager.start(coverIntent);
|
|
221
|
+
expect(typeof id).toBe('string');
|
|
222
|
+
|
|
223
|
+
const events = await collectUntilDone(manager, id);
|
|
224
|
+
expect(events[0]).toEqual({ type: 'started' });
|
|
225
|
+
expect(events[events.length - 1].type).toBe('done');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('rejects a cover-operation run for an unknown flow', () => {
|
|
229
|
+
const manager = new AgentRunManager(projectDir, flowsDir, pathOf);
|
|
230
|
+
const badIntent: AgentIntent = { action: 'cover-operation', flowId: 'nope', instruction: 'x' };
|
|
231
|
+
expect(() => manager.start(badIntent)).toThrow(/unknown flow/i);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('routes to spawnClaude when agent is "claude", using EMBERFLOW_CLAUDE_BIN', async () => {
|
|
235
|
+
process.env.EMBERFLOW_CLAUDE_BIN = fakeClaudeWriterPath;
|
|
236
|
+
try {
|
|
237
|
+
const manager = new AgentRunManager(projectDir, flowsDir, pathOf);
|
|
238
|
+
const id = manager.start(intent, { agent: 'claude' });
|
|
239
|
+
|
|
240
|
+
const events = await collectUntilDone(manager, id);
|
|
241
|
+
expect(events[0]).toEqual({ type: 'started' });
|
|
242
|
+
expect(events[events.length - 1].type).toBe('done');
|
|
243
|
+
|
|
244
|
+
const { files } = manager.diff(id)!;
|
|
245
|
+
expect(files).toContain('flows/hello.json');
|
|
246
|
+
expect(readFileSync(join(projectDir, 'flows', 'hello.json'), 'utf8')).toBe('agent was here\n');
|
|
247
|
+
} finally {
|
|
248
|
+
delete process.env.EMBERFLOW_CLAUDE_BIN;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { changedFiles, diffSince, isGitRepo, revert as gitRevert, snapshot, type GitSnapshot } from './gitScope';
|
|
3
|
+
import { buildPrompt, type AgentIntent, type AvailableNode } from './prompt';
|
|
4
|
+
import type { InfrastructureManifest } from '../infrastructure';
|
|
5
|
+
import { spawnCodex, type SpawnedAgent } from './codexAdapter';
|
|
6
|
+
import { spawnClaude } from './claudeAdapter';
|
|
7
|
+
import type { AgentEvent, AgentKind } from './types';
|
|
8
|
+
import { isPathWithin } from '../pathSafety';
|
|
9
|
+
|
|
10
|
+
export interface StartAgentOptions {
|
|
11
|
+
agent?: AgentKind;
|
|
12
|
+
model?: string;
|
|
13
|
+
reasoning?: 'low' | 'medium' | 'high';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Thrown by `start()`; `status` maps directly onto the HTTP status the endpoint should return. */
|
|
17
|
+
export class AgentStartError extends Error {
|
|
18
|
+
constructor(
|
|
19
|
+
message: string,
|
|
20
|
+
readonly status: number,
|
|
21
|
+
) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'AgentStartError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Listener = (event: AgentEvent) => void;
|
|
28
|
+
|
|
29
|
+
/** Max number of terminal (done/error) runs kept around for diff/revert/replay before eviction. */
|
|
30
|
+
const MAX_TERMINAL_RUNS = 20;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Rejects anything that isn't a safe flow id. Operation ids are path-style
|
|
34
|
+
* (e.g. `"billing/charge"`, the on-disk relative path), so a slash-separated
|
|
35
|
+
* id is legitimate and must be allowed — only traversal (`..` segments),
|
|
36
|
+
* absolute ids, backslashes, and empty segments are rejected, and the file it
|
|
37
|
+
* resolves to must stay inside `apisDir`. Defense in depth — the agent
|
|
38
|
+
* sandbox already confines actual writes — but it keeps a bogus flowId like
|
|
39
|
+
* `"../../etc/passwd"` from ever becoming an existence-probe against paths
|
|
40
|
+
* outside apisDir.
|
|
41
|
+
*/
|
|
42
|
+
function isSafeFlowId(flowId: string, apisDir: string): boolean {
|
|
43
|
+
if (!flowId) return false;
|
|
44
|
+
return isPathWithin(apisDir, `${flowId}.json`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolves a `new-operation` intent's user-supplied `location` (a folder path
|
|
49
|
+
* under `apisDir`, e.g. `"billing"` or `"billing/charges"`) to the directory
|
|
50
|
+
* the new operation file should be created in. An empty string or `"default"`
|
|
51
|
+
* both mean "the default API".
|
|
52
|
+
*/
|
|
53
|
+
function resolveLocationDir(location: string): string {
|
|
54
|
+
const trimmed = location.trim();
|
|
55
|
+
return trimmed === '' ? 'default' : trimmed;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Same defense-in-depth as `isSafeFlowId`, but for a target *directory* rather than a flow file. */
|
|
59
|
+
function isSafeLocationDir(dir: string, apisDir: string): boolean {
|
|
60
|
+
return isPathWithin(apisDir, dir);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface Run {
|
|
64
|
+
id: string;
|
|
65
|
+
snapshot: GitSnapshot;
|
|
66
|
+
adapter: SpawnedAgent;
|
|
67
|
+
buffer: AgentEvent[];
|
|
68
|
+
listeners: Set<Listener>;
|
|
69
|
+
status: 'running' | 'done' | 'error';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Owns in-flight coding-agent runs for a single project: spawns the adapter,
|
|
74
|
+
* buffers its events for SSE replay + live subscribers, and exposes
|
|
75
|
+
* git-scoped diff/revert against the snapshot taken at run start. One active
|
|
76
|
+
* run per project at a time.
|
|
77
|
+
*/
|
|
78
|
+
export class AgentRunManager {
|
|
79
|
+
private readonly runs = new Map<string, Run>();
|
|
80
|
+
private activeRunId: string | null = null;
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
private readonly projectDir: string,
|
|
84
|
+
private readonly apisDir: string,
|
|
85
|
+
private readonly pathOf: (id: string) => string | undefined,
|
|
86
|
+
/**
|
|
87
|
+
* Getter (not a static list) so the prompt always reflects the
|
|
88
|
+
* currently-registered node types, even if the registry is populated or
|
|
89
|
+
* changes after this manager is constructed.
|
|
90
|
+
*/
|
|
91
|
+
private readonly availableNodes: () => AvailableNode[] = () => [],
|
|
92
|
+
/** The loaded project's authored language (`ProjectConfig.language`,
|
|
93
|
+
* explicit-or-inferred), threaded into every prompt this manager builds.
|
|
94
|
+
* Defaults to 'typescript' — the Emberflow repo itself, when constructed
|
|
95
|
+
* without a loaded consumer project. */
|
|
96
|
+
private readonly projectLanguage: 'javascript' | 'typescript' = 'typescript',
|
|
97
|
+
/**
|
|
98
|
+
* Getter (not a static value) for the loaded `emberflow/infrastructure.json`
|
|
99
|
+
* manifest, so every prompt reflects the manifest currently on disk — a scout
|
|
100
|
+
* run mid-session updates what later runs see. Returns null when the project
|
|
101
|
+
* hasn't been scouted or the file is malformed. Injected as a prompt preamble
|
|
102
|
+
* so agents REUSE known infrastructure instead of inventing parallel config.
|
|
103
|
+
*/
|
|
104
|
+
private readonly infrastructure: () => InfrastructureManifest | null = () => null,
|
|
105
|
+
) {}
|
|
106
|
+
|
|
107
|
+
/** Validates the project + flow, snapshots git, spawns the adapter, and returns the new run's id. */
|
|
108
|
+
start(intent: AgentIntent, opts: StartAgentOptions = {}): string {
|
|
109
|
+
if (this.activeRunId) {
|
|
110
|
+
throw new AgentStartError('An agent run is already active for this project', 409);
|
|
111
|
+
}
|
|
112
|
+
if (!isGitRepo(this.projectDir)) {
|
|
113
|
+
throw new AgentStartError(`${this.projectDir} is not a git repository`, 400);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// `new-operation` creates a brand-new flow, so there's no existing
|
|
117
|
+
// flowId to validate against pathOf — it validates `location` instead.
|
|
118
|
+
// `setup-auth` targets an environment, not a flow file, and
|
|
119
|
+
// `setup-environments` targets the environments file itself, so
|
|
120
|
+
// neither has a relPath to resolve.
|
|
121
|
+
let relPath: string;
|
|
122
|
+
if (intent.action === 'new-operation') {
|
|
123
|
+
const dir = resolveLocationDir(intent.location);
|
|
124
|
+
if (!isSafeLocationDir(dir, this.apisDir)) {
|
|
125
|
+
throw new AgentStartError('Invalid location', 400);
|
|
126
|
+
}
|
|
127
|
+
relPath = dir;
|
|
128
|
+
} else if (
|
|
129
|
+
intent.action === 'setup-auth' ||
|
|
130
|
+
intent.action === 'setup-environments' ||
|
|
131
|
+
intent.action === 'scout-infrastructure'
|
|
132
|
+
) {
|
|
133
|
+
// No flow file: setup-auth targets an environment, setup-environments the
|
|
134
|
+
// environments file, and scout-infrastructure reads the whole project and
|
|
135
|
+
// writes emberflow/infrastructure.json — none has a relPath to resolve.
|
|
136
|
+
relPath = '';
|
|
137
|
+
} else if (intent.action === 'ask') {
|
|
138
|
+
// `ask`'s flowId is optional: with one, resolve the op's relPath like
|
|
139
|
+
// edit-flow does; without, resolve to the apis root (empty relPath) —
|
|
140
|
+
// there's no specific operation file to point the prompt at.
|
|
141
|
+
if (intent.flowId) {
|
|
142
|
+
if (!isSafeFlowId(intent.flowId, this.apisDir)) {
|
|
143
|
+
throw new AgentStartError('Invalid flowId', 400);
|
|
144
|
+
}
|
|
145
|
+
const found = this.pathOf(intent.flowId);
|
|
146
|
+
if (!found) {
|
|
147
|
+
throw new AgentStartError(`Unknown flow: ${intent.flowId}`, 400);
|
|
148
|
+
}
|
|
149
|
+
relPath = found;
|
|
150
|
+
} else {
|
|
151
|
+
relPath = '';
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
if (!isSafeFlowId(intent.flowId, this.apisDir)) {
|
|
155
|
+
throw new AgentStartError('Invalid flowId', 400);
|
|
156
|
+
}
|
|
157
|
+
const found = this.pathOf(intent.flowId);
|
|
158
|
+
if (!found) {
|
|
159
|
+
throw new AgentStartError(`Unknown flow: ${intent.flowId}`, 400);
|
|
160
|
+
}
|
|
161
|
+
relPath = found;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.pruneTerminalRuns();
|
|
165
|
+
|
|
166
|
+
const snap = snapshot(this.projectDir);
|
|
167
|
+
const prompt = buildPrompt(
|
|
168
|
+
intent,
|
|
169
|
+
this.apisDir,
|
|
170
|
+
relPath,
|
|
171
|
+
this.availableNodes(),
|
|
172
|
+
this.projectLanguage,
|
|
173
|
+
this.infrastructure(),
|
|
174
|
+
);
|
|
175
|
+
const adapter =
|
|
176
|
+
opts.agent === 'claude'
|
|
177
|
+
? spawnClaude(prompt, this.projectDir, {
|
|
178
|
+
model: opts.model,
|
|
179
|
+
bin: process.env.EMBERFLOW_CLAUDE_BIN,
|
|
180
|
+
})
|
|
181
|
+
: spawnCodex(prompt, this.projectDir, {
|
|
182
|
+
model: opts.model,
|
|
183
|
+
reasoning: opts.reasoning ?? 'medium',
|
|
184
|
+
bin: process.env.EMBERFLOW_CODEX_BIN,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const id = randomUUID();
|
|
188
|
+
const run: Run = { id, snapshot: snap, adapter, buffer: [], listeners: new Set(), status: 'running' };
|
|
189
|
+
this.runs.set(id, run);
|
|
190
|
+
this.activeRunId = id;
|
|
191
|
+
|
|
192
|
+
void this.consume(run);
|
|
193
|
+
|
|
194
|
+
return id;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Drains the adapter's events into the run's buffer, fanning each out to live subscribers. */
|
|
198
|
+
private async consume(run: Run): Promise<void> {
|
|
199
|
+
const emit = (event: AgentEvent): void => {
|
|
200
|
+
run.buffer.push(event);
|
|
201
|
+
for (const listener of run.listeners) listener(event);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
for await (const event of run.adapter.events) {
|
|
206
|
+
// Clear the busy slot as soon as a terminal event is *observed*, not
|
|
207
|
+
// once the adapter's generator fully drains — a subscriber reacting
|
|
208
|
+
// synchronously to `done`/`error` (e.g. by starting a new run) must
|
|
209
|
+
// not race against this loop's own bookkeeping.
|
|
210
|
+
if (event.type === 'done') {
|
|
211
|
+
run.status = 'done';
|
|
212
|
+
this.clearActive(run.id);
|
|
213
|
+
} else if (event.type === 'error') {
|
|
214
|
+
run.status = 'error';
|
|
215
|
+
this.clearActive(run.id);
|
|
216
|
+
}
|
|
217
|
+
emit(event);
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
run.status = 'error';
|
|
221
|
+
this.clearActive(run.id);
|
|
222
|
+
emit({ type: 'error', text: err instanceof Error ? err.message : String(err) });
|
|
223
|
+
} finally {
|
|
224
|
+
this.clearActive(run.id);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private clearActive(id: string): void {
|
|
229
|
+
if (this.activeRunId === id) this.activeRunId = null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Replay the buffer to the listener, then stream live events. Returns an unsubscribe fn. */
|
|
233
|
+
subscribe(id: string, listener: Listener): (() => void) | undefined {
|
|
234
|
+
const run = this.runs.get(id);
|
|
235
|
+
if (!run) return undefined;
|
|
236
|
+
for (const event of run.buffer) listener(event);
|
|
237
|
+
run.listeners.add(listener);
|
|
238
|
+
return () => run.listeners.delete(listener);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
has(id: string): boolean {
|
|
242
|
+
return this.runs.has(id);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
diff(id: string): { diff: string; files: string[] } | undefined {
|
|
246
|
+
const run = this.runs.get(id);
|
|
247
|
+
if (!run) return undefined;
|
|
248
|
+
return {
|
|
249
|
+
diff: diffSince(this.projectDir, run.snapshot),
|
|
250
|
+
files: changedFiles(this.projectDir, run.snapshot),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
revert(id: string): { reverted: string[] } | undefined {
|
|
255
|
+
const run = this.runs.get(id);
|
|
256
|
+
if (!run) return undefined;
|
|
257
|
+
const files = changedFiles(this.projectDir, run.snapshot);
|
|
258
|
+
gitRevert(this.projectDir, run.snapshot, files);
|
|
259
|
+
return { reverted: files };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
cancel(id: string): boolean {
|
|
263
|
+
const run = this.runs.get(id);
|
|
264
|
+
if (!run) return false;
|
|
265
|
+
run.adapter.cancel();
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Evicts oldest terminal (done|error) runs beyond MAX_TERMINAL_RUNS so the
|
|
271
|
+
* `runs` map — and each run's full event buffer — doesn't grow unbounded
|
|
272
|
+
* over a long-lived server process. The active run (if any) is never
|
|
273
|
+
* evicted; insertion order on the Map gives us oldest-first for free.
|
|
274
|
+
*/
|
|
275
|
+
private pruneTerminalRuns(): void {
|
|
276
|
+
const terminalIds: string[] = [];
|
|
277
|
+
for (const [id, run] of this.runs) {
|
|
278
|
+
if (run.status === 'done' || run.status === 'error') terminalIds.push(id);
|
|
279
|
+
}
|
|
280
|
+
const excess = terminalIds.length - MAX_TERMINAL_RUNS;
|
|
281
|
+
if (excess <= 0) return;
|
|
282
|
+
for (const id of terminalIds.slice(0, excess)) {
|
|
283
|
+
this.runs.delete(id);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type AgentKind = 'codex' | 'claude';
|
|
2
|
+
|
|
3
|
+
export interface AgentEvent {
|
|
4
|
+
type: 'started' | 'message' | 'command' | 'mcp' | 'approval-request' | 'done' | 'error';
|
|
5
|
+
text?: string; // message/error text
|
|
6
|
+
command?: string; // for 'command'
|
|
7
|
+
commandStatus?: 'in_progress' | 'completed' | 'failed';
|
|
8
|
+
exitCode?: number | null; // for 'command'
|
|
9
|
+
output?: string; // command aggregated output OR an mcp call's result
|
|
10
|
+
toolUseId?: string; // for 'command'/'mcp' (Claude) — correlates a tool_use with its later tool_result
|
|
11
|
+
approvalId?: string; // for 'approval-request' (Claude)
|
|
12
|
+
usage?: Record<string, number>; // for 'done'
|
|
13
|
+
// ── 'mcp': the agent called an Emberflow MCP tool (run/list/delete/etc.) ──
|
|
14
|
+
mcpServer?: string; // e.g. 'emberflow'
|
|
15
|
+
mcpTool?: string; // e.g. 'run_operation'
|
|
16
|
+
mcpStatus?: 'in_progress' | 'completed' | 'failed';
|
|
17
|
+
}
|