@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,162 @@
|
|
|
1
|
+
/** Best-effort extraction of a shell command from a Bash tool_use input. */
|
|
2
|
+
function bashCommandFrom(block) {
|
|
3
|
+
const input = block.input;
|
|
4
|
+
if (!input)
|
|
5
|
+
return undefined;
|
|
6
|
+
const command = input.command;
|
|
7
|
+
return typeof command === 'string' ? command : undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Fallback label for a tool whose input we don't format specially (or that's
|
|
10
|
+
* missing its expected field): `<name> <compact-json>` capped at 80 chars. */
|
|
11
|
+
function compactLabel(name, input) {
|
|
12
|
+
const json = JSON.stringify(input ?? {});
|
|
13
|
+
return `${name} ${json.length > 80 ? json.slice(0, 80) : json}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Human label for a non-mcp tool_use block, used as a `command` event's
|
|
17
|
+
* `command` string so Read/Edit/Grep/… fold into the UI's "Ran N commands"
|
|
18
|
+
* group instead of leaking as reasoning prose. Bash keeps its raw shell command;
|
|
19
|
+
* the common Claude tools get a purpose-built one-liner; anything else gets the
|
|
20
|
+
* compact `<name> <json>` fallback.
|
|
21
|
+
*/
|
|
22
|
+
function commandLabelFor(block) {
|
|
23
|
+
const name = block.name ?? 'tool';
|
|
24
|
+
const input = (block.input ?? {});
|
|
25
|
+
const str = (v) => (typeof v === 'string' ? v : undefined);
|
|
26
|
+
switch (name) {
|
|
27
|
+
case 'Bash':
|
|
28
|
+
return bashCommandFrom(block) ?? compactLabel(name, input);
|
|
29
|
+
case 'Read': {
|
|
30
|
+
const fp = str(input.file_path);
|
|
31
|
+
if (!fp)
|
|
32
|
+
return compactLabel(name, input);
|
|
33
|
+
const bits = [];
|
|
34
|
+
if (typeof input.offset === 'number')
|
|
35
|
+
bits.push(`offset ${input.offset}`);
|
|
36
|
+
if (typeof input.limit === 'number')
|
|
37
|
+
bits.push(`limit ${input.limit}`);
|
|
38
|
+
return `Read ${fp}${bits.length ? ` (${bits.join(', ')})` : ''}`;
|
|
39
|
+
}
|
|
40
|
+
case 'Edit':
|
|
41
|
+
case 'Write': {
|
|
42
|
+
const fp = str(input.file_path);
|
|
43
|
+
return fp ? `${name} ${fp}` : compactLabel(name, input);
|
|
44
|
+
}
|
|
45
|
+
case 'Grep': {
|
|
46
|
+
const pattern = str(input.pattern);
|
|
47
|
+
if (!pattern)
|
|
48
|
+
return compactLabel(name, input);
|
|
49
|
+
const path = str(input.path);
|
|
50
|
+
return `Grep ${pattern}${path ? ` ${path}` : ''}`;
|
|
51
|
+
}
|
|
52
|
+
case 'Glob': {
|
|
53
|
+
const pattern = str(input.pattern);
|
|
54
|
+
return pattern ? `Glob ${pattern}` : compactLabel(name, input);
|
|
55
|
+
}
|
|
56
|
+
case 'TodoWrite':
|
|
57
|
+
return 'update todos';
|
|
58
|
+
default:
|
|
59
|
+
return compactLabel(name, input);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Flattens a tool_result's `content` (string, or array of text blocks) to plain text. */
|
|
63
|
+
function toolResultTextFrom(block) {
|
|
64
|
+
const content = block.content;
|
|
65
|
+
if (typeof content === 'string')
|
|
66
|
+
return content;
|
|
67
|
+
if (Array.isArray(content)) {
|
|
68
|
+
return content
|
|
69
|
+
.filter((c) => c.type === 'text' && typeof c.text === 'string')
|
|
70
|
+
.map((c) => c.text)
|
|
71
|
+
.join('');
|
|
72
|
+
}
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
export function parseClaudeLine(line) {
|
|
76
|
+
const trimmed = line.trim();
|
|
77
|
+
if (!trimmed)
|
|
78
|
+
return [];
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(trimmed);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
switch (parsed.type) {
|
|
87
|
+
case 'system': {
|
|
88
|
+
if (parsed.subtype === 'init')
|
|
89
|
+
return [{ type: 'started' }];
|
|
90
|
+
// hook_started / hook_response and any other system noise — ignore.
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
case 'assistant': {
|
|
94
|
+
const blocks = parsed.message?.content ?? [];
|
|
95
|
+
// Claude routinely emits multiple content blocks per message (e.g.
|
|
96
|
+
// explain-then-call `[text, tool_use]`, or parallel tool calls) — emit
|
|
97
|
+
// one AgentEvent per meaningful block, in order, so the console shows the
|
|
98
|
+
// full picture. `thinking` and other block types are skipped.
|
|
99
|
+
const events = [];
|
|
100
|
+
for (const block of blocks) {
|
|
101
|
+
if (block.type === 'text' && block.text) {
|
|
102
|
+
events.push({ type: 'message', text: block.text });
|
|
103
|
+
}
|
|
104
|
+
else if (block.type === 'tool_use') {
|
|
105
|
+
// MCP tools surface as `mcp__<server>__<tool>` — show them as MCP calls.
|
|
106
|
+
if (block.name?.startsWith('mcp__')) {
|
|
107
|
+
const [, server, ...toolParts] = block.name.split('__');
|
|
108
|
+
events.push({
|
|
109
|
+
type: 'mcp',
|
|
110
|
+
mcpServer: server || 'mcp',
|
|
111
|
+
mcpTool: toolParts.join('__') || 'tool',
|
|
112
|
+
mcpStatus: 'in_progress',
|
|
113
|
+
...(block.id ? { toolUseId: block.id } : {}),
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Every other tool (Bash, Read, Edit, Grep, Glob, TodoWrite, …) is a
|
|
118
|
+
// `command` event with a human label, correlated by tool_use_id like
|
|
119
|
+
// Bash always was — so it folds into the UI's "Ran N commands" group
|
|
120
|
+
// instead of leaking as reasoning prose.
|
|
121
|
+
events.push({
|
|
122
|
+
type: 'command',
|
|
123
|
+
command: commandLabelFor(block),
|
|
124
|
+
commandStatus: 'in_progress',
|
|
125
|
+
...(block.id ? { toolUseId: block.id } : {}),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return events;
|
|
130
|
+
}
|
|
131
|
+
case 'user': {
|
|
132
|
+
// Tool results. Emit a partial completion carrying the tool_use_id +
|
|
133
|
+
// output text + status; the adapter looks up the original command text
|
|
134
|
+
// by id (it isn't repeated in the tool_result line) and skips gracefully
|
|
135
|
+
// if the id doesn't correspond to a command it's tracking (e.g. a
|
|
136
|
+
// non-Bash tool result).
|
|
137
|
+
const blocks = parsed.message?.content ?? [];
|
|
138
|
+
const events = [];
|
|
139
|
+
for (const block of blocks) {
|
|
140
|
+
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
141
|
+
events.push({
|
|
142
|
+
type: 'command',
|
|
143
|
+
toolUseId: block.tool_use_id,
|
|
144
|
+
commandStatus: block.is_error ? 'failed' : 'completed',
|
|
145
|
+
output: toolResultTextFrom(block),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return events;
|
|
150
|
+
}
|
|
151
|
+
case 'result': {
|
|
152
|
+
if (parsed.subtype === 'error' || parsed.is_error) {
|
|
153
|
+
return [{ type: 'error', text: parsed.result ?? 'error' }];
|
|
154
|
+
}
|
|
155
|
+
return [{ type: 'done', usage: parsed.usage }];
|
|
156
|
+
}
|
|
157
|
+
case 'rate_limit_event':
|
|
158
|
+
return [];
|
|
159
|
+
default:
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AgentEvent } from './types.js';
|
|
2
|
+
export interface SpawnedAgent {
|
|
3
|
+
events: AsyncIterable<AgentEvent>;
|
|
4
|
+
cancel(): void;
|
|
5
|
+
}
|
|
6
|
+
export interface CodexOptions {
|
|
7
|
+
model?: string;
|
|
8
|
+
reasoning?: 'low' | 'medium' | 'high';
|
|
9
|
+
bin?: string;
|
|
10
|
+
/** Grace period (ms) between SIGTERM and the SIGKILL escalation in cancel(). Test hook. */
|
|
11
|
+
killGraceMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function spawnCodex(prompt: string, projectDir: string, opts?: CodexOptions): SpawnedAgent;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import { parseCodexLine } from './codexParse.js';
|
|
4
|
+
import { modelRejectionHint } from './modelRejectionHint.js';
|
|
5
|
+
const STDERR_TAIL_BYTES = 2048;
|
|
6
|
+
export function spawnCodex(prompt, projectDir, opts = {}) {
|
|
7
|
+
const bin = opts.bin ?? 'codex';
|
|
8
|
+
const args = [
|
|
9
|
+
// Never ask for per-call approval — the studio runs codex non-interactively,
|
|
10
|
+
// so an approval prompt has no one to answer it and codex cancels the call
|
|
11
|
+
// ("user cancelled MCP tool call"). This is a top-level flag (before `exec`).
|
|
12
|
+
// The sandbox (-s workspace-write, below) still constrains what can run.
|
|
13
|
+
'-a',
|
|
14
|
+
'never',
|
|
15
|
+
'exec',
|
|
16
|
+
'-C',
|
|
17
|
+
projectDir,
|
|
18
|
+
'-s',
|
|
19
|
+
'workspace-write',
|
|
20
|
+
// Keep the workspace-write FILE sandbox, but allow network — the agent's CLI
|
|
21
|
+
// ops (node bin/emberflow.mjs run/get-workflow/…) must reach the runner on
|
|
22
|
+
// 127.0.0.1; workspace-write blocks all network by default (curl → 000).
|
|
23
|
+
'-c',
|
|
24
|
+
'sandbox_workspace_write.network_access=true',
|
|
25
|
+
'-c',
|
|
26
|
+
`model_reasoning_effort=${opts.reasoning ?? 'low'}`,
|
|
27
|
+
'--json',
|
|
28
|
+
...(opts.model ? ['-m', opts.model] : []),
|
|
29
|
+
prompt,
|
|
30
|
+
];
|
|
31
|
+
// detached: give codex its own process group so cancel() can tree-kill any
|
|
32
|
+
// subprocesses it spawns (workspace-write), not just the top-level process.
|
|
33
|
+
// stdio ['ignore','pipe','pipe'] → stdin is null, stdout/stderr are Readable.
|
|
34
|
+
const child = bin.endsWith('.mjs')
|
|
35
|
+
? spawn(process.execPath, [bin, ...args], { stdio: ['ignore', 'pipe', 'pipe'], detached: true })
|
|
36
|
+
: spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'], detached: true });
|
|
37
|
+
// Buffer only the last ~2KB of stderr — non-fatal in general (e.g. the user's
|
|
38
|
+
// global MCP servers spew auth errors there), but useful as a diagnostic tail
|
|
39
|
+
// when the process dies without a terminal event (see maybeFinalize below).
|
|
40
|
+
let stderrTail = '';
|
|
41
|
+
child.stderr.on('data', (chunk) => {
|
|
42
|
+
stderrTail = (stderrTail + chunk.toString('utf8')).slice(-STDERR_TAIL_BYTES);
|
|
43
|
+
});
|
|
44
|
+
const queue = [];
|
|
45
|
+
const resolvers = [];
|
|
46
|
+
let done = false;
|
|
47
|
+
let sawTerminal = false;
|
|
48
|
+
function push(event) {
|
|
49
|
+
queue.push(event);
|
|
50
|
+
const resolve = resolvers.shift();
|
|
51
|
+
if (resolve)
|
|
52
|
+
resolve();
|
|
53
|
+
}
|
|
54
|
+
function finish() {
|
|
55
|
+
done = true;
|
|
56
|
+
while (resolvers.length) {
|
|
57
|
+
const resolve = resolvers.shift();
|
|
58
|
+
if (resolve)
|
|
59
|
+
resolve();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const rl = readline.createInterface({ input: child.stdout });
|
|
63
|
+
rl.on('line', (line) => {
|
|
64
|
+
const event = parseCodexLine(line);
|
|
65
|
+
if (event) {
|
|
66
|
+
if (event.type === 'done' || event.type === 'error')
|
|
67
|
+
sawTerminal = true;
|
|
68
|
+
push(event);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// Finalize only once BOTH the stdout stream is fully drained (readline 'close')
|
|
72
|
+
// AND the process has exited ('close', which fires after all stdio is flushed —
|
|
73
|
+
// unlike 'exit', which can fire while trailing stdout lines are still buffered).
|
|
74
|
+
let rlClosed = false;
|
|
75
|
+
let procClosed = false;
|
|
76
|
+
let exitCode = null;
|
|
77
|
+
let escalateTimer;
|
|
78
|
+
function clearEscalation() {
|
|
79
|
+
if (escalateTimer) {
|
|
80
|
+
clearTimeout(escalateTimer);
|
|
81
|
+
escalateTimer = undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function maybeFinalize() {
|
|
85
|
+
if (!rlClosed || !procClosed)
|
|
86
|
+
return;
|
|
87
|
+
clearEscalation();
|
|
88
|
+
if (!sawTerminal) {
|
|
89
|
+
if (exitCode === 0) {
|
|
90
|
+
// Clean exit but the model never emitted turn.completed — synthesize a done.
|
|
91
|
+
push({ type: 'done' });
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const hint = modelRejectionHint('codex', stderrTail);
|
|
95
|
+
const text = hint ? `codex exited with code ${exitCode} (${hint})` : `codex exited with code ${exitCode}`;
|
|
96
|
+
push({ type: 'error', text });
|
|
97
|
+
}
|
|
98
|
+
sawTerminal = true;
|
|
99
|
+
}
|
|
100
|
+
finish();
|
|
101
|
+
}
|
|
102
|
+
rl.on('close', () => {
|
|
103
|
+
rlClosed = true;
|
|
104
|
+
maybeFinalize();
|
|
105
|
+
});
|
|
106
|
+
child.on('close', (code) => {
|
|
107
|
+
procClosed = true;
|
|
108
|
+
exitCode = code;
|
|
109
|
+
maybeFinalize();
|
|
110
|
+
});
|
|
111
|
+
// A failed spawn (missing binary, bad EMBERFLOW_*_BIN, permissions, ...) emits
|
|
112
|
+
// an 'error' event on the child instead of ever exiting/closing normally. Left
|
|
113
|
+
// unhandled, Node rethrows it and crashes the whole runner process — so we
|
|
114
|
+
// must handle it and finalize the stream ourselves. 'close' still fires after
|
|
115
|
+
// a spawn error (with code null), so guard against double-finalizing there.
|
|
116
|
+
let spawnFailed = false;
|
|
117
|
+
child.on('error', (err) => {
|
|
118
|
+
if (spawnFailed)
|
|
119
|
+
return;
|
|
120
|
+
spawnFailed = true;
|
|
121
|
+
clearEscalation();
|
|
122
|
+
push({ type: 'error', text: `failed to start codex: ${err.message}` });
|
|
123
|
+
sawTerminal = true;
|
|
124
|
+
rlClosed = true;
|
|
125
|
+
procClosed = true;
|
|
126
|
+
finish();
|
|
127
|
+
});
|
|
128
|
+
async function* generate() {
|
|
129
|
+
while (true) {
|
|
130
|
+
if (queue.length > 0) {
|
|
131
|
+
yield queue.shift();
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (done)
|
|
135
|
+
return;
|
|
136
|
+
await new Promise((resolve) => resolvers.push(resolve));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Kill the whole process group (negative pid) so codex's own subprocesses die
|
|
140
|
+
// too; fall back to a plain kill if the group signal fails (e.g. already gone
|
|
141
|
+
// or no group). Used by both the initial SIGTERM and the SIGKILL escalation.
|
|
142
|
+
function killGroup(signal) {
|
|
143
|
+
try {
|
|
144
|
+
if (child.pid != null) {
|
|
145
|
+
process.kill(-child.pid, signal);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// group kill failed — fall through to a direct kill
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
child.kill(signal);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// process already gone — nothing to do
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
events: { [Symbol.asyncIterator]: generate },
|
|
161
|
+
cancel() {
|
|
162
|
+
killGroup('SIGTERM');
|
|
163
|
+
// If the process (or a stubborn grandchild/MCP server) ignores SIGTERM,
|
|
164
|
+
// escalate to SIGKILL after a grace period so a hung agent can't
|
|
165
|
+
// permanently lock the run slot. Cleared once close/error finalizes.
|
|
166
|
+
clearEscalation();
|
|
167
|
+
escalateTimer = setTimeout(() => {
|
|
168
|
+
escalateTimer = undefined;
|
|
169
|
+
killGroup('SIGKILL');
|
|
170
|
+
}, opts.killGraceMs ?? 3000);
|
|
171
|
+
escalateTimer.unref?.();
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Map a codex mcp_tool_call item → an 'mcp' AgentEvent, tolerating shape drift. */
|
|
2
|
+
function mcpEventFrom(item) {
|
|
3
|
+
const server = item.server ?? item.invocation?.server ?? 'mcp';
|
|
4
|
+
const tool = item.tool ?? item.name ?? item.invocation?.tool ?? 'tool';
|
|
5
|
+
const raw = item.error ?? item.result ?? item.output;
|
|
6
|
+
const output = raw === undefined ? undefined : typeof raw === 'string' ? raw : JSON.stringify(raw, null, 2);
|
|
7
|
+
return { type: 'mcp', mcpServer: server, mcpTool: tool, mcpStatus: item.status, ...(output !== undefined ? { output } : {}) };
|
|
8
|
+
}
|
|
9
|
+
export function parseCodexLine(line) {
|
|
10
|
+
const trimmed = line.trim();
|
|
11
|
+
if (!trimmed)
|
|
12
|
+
return null;
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = JSON.parse(trimmed);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
switch (parsed.type) {
|
|
21
|
+
case 'thread.started':
|
|
22
|
+
case 'turn.started':
|
|
23
|
+
return { type: 'started' };
|
|
24
|
+
case 'item.started': {
|
|
25
|
+
const item = parsed.item;
|
|
26
|
+
if (item && item.type === 'command_execution') {
|
|
27
|
+
const commandItem = item;
|
|
28
|
+
return {
|
|
29
|
+
type: 'command',
|
|
30
|
+
command: commandItem.command,
|
|
31
|
+
commandStatus: commandItem.status,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (item && item.type === 'mcp_tool_call')
|
|
35
|
+
return mcpEventFrom(item);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
case 'item.completed': {
|
|
39
|
+
const item = parsed.item;
|
|
40
|
+
if (!item)
|
|
41
|
+
return null;
|
|
42
|
+
if (item.type === 'command_execution') {
|
|
43
|
+
const commandItem = item;
|
|
44
|
+
return {
|
|
45
|
+
type: 'command',
|
|
46
|
+
command: commandItem.command,
|
|
47
|
+
exitCode: commandItem.exit_code,
|
|
48
|
+
output: commandItem.aggregated_output,
|
|
49
|
+
commandStatus: commandItem.status,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (item.type === 'mcp_tool_call')
|
|
53
|
+
return mcpEventFrom(item);
|
|
54
|
+
if (item.type === 'agent_message') {
|
|
55
|
+
const messageItem = item;
|
|
56
|
+
return { type: 'message', text: messageItem.text };
|
|
57
|
+
}
|
|
58
|
+
if (item.type === 'error') {
|
|
59
|
+
// A codex `error` ITEM is a non-fatal diagnostic (e.g. an MCP server
|
|
60
|
+
// failing to auth) — NOT the run failing. Surface it as a message so it
|
|
61
|
+
// stays visible without terminating the run. A real failure is signalled
|
|
62
|
+
// by the process exiting non-zero with no `done` (see codexAdapter),
|
|
63
|
+
// which is the ONLY thing that yields a terminal AgentEvent 'error'.
|
|
64
|
+
const errorItem = item;
|
|
65
|
+
return { type: 'message', text: `⚠ ${errorItem.text ?? errorItem.message ?? 'error'}` };
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
case 'turn.completed':
|
|
70
|
+
return { type: 'done', usage: parsed.usage };
|
|
71
|
+
default:
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AgentKind } from './types.js';
|
|
2
|
+
export interface DetectedAgent {
|
|
3
|
+
kind: AgentKind;
|
|
4
|
+
version: string | null;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Real PATH probe: runs `<bin> --version` and parses the first semver-ish
|
|
8
|
+
* token out of stdout. Returns `undefined` when the binary isn't present/
|
|
9
|
+
* runnable (spawn error, timeout, or nonzero exit); `null` version when it
|
|
10
|
+
* ran but produced no parseable version string.
|
|
11
|
+
*/
|
|
12
|
+
export declare function probe(bin: string): {
|
|
13
|
+
version: string | null;
|
|
14
|
+
} | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Detects which coding-agent CLIs are available on PATH, and the version
|
|
17
|
+
* each reports (null when present but unparseable). `probeBin` is
|
|
18
|
+
* injectable so tests can stub PATH presence/version without shelling out
|
|
19
|
+
* for real binaries.
|
|
20
|
+
*/
|
|
21
|
+
export declare function detectAgents(probeBin?: (bin: string) => {
|
|
22
|
+
version: string | null;
|
|
23
|
+
} | undefined): DetectedAgent[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
const CANDIDATES = ['codex', 'claude'];
|
|
3
|
+
const VERSION_RE = /\d+\.\d+\.\d+/;
|
|
4
|
+
/**
|
|
5
|
+
* Real PATH probe: runs `<bin> --version` and parses the first semver-ish
|
|
6
|
+
* token out of stdout. Returns `undefined` when the binary isn't present/
|
|
7
|
+
* runnable (spawn error, timeout, or nonzero exit); `null` version when it
|
|
8
|
+
* ran but produced no parseable version string.
|
|
9
|
+
*/
|
|
10
|
+
export function probe(bin) {
|
|
11
|
+
const result = spawnSync(bin, ['--version'], {
|
|
12
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
13
|
+
timeout: 3000,
|
|
14
|
+
encoding: 'utf8',
|
|
15
|
+
});
|
|
16
|
+
if (result.error !== undefined || result.status !== 0)
|
|
17
|
+
return undefined;
|
|
18
|
+
const match = result.stdout?.match(VERSION_RE);
|
|
19
|
+
return { version: match ? match[0] : null };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Detects which coding-agent CLIs are available on PATH, and the version
|
|
23
|
+
* each reports (null when present but unparseable). `probeBin` is
|
|
24
|
+
* injectable so tests can stub PATH presence/version without shelling out
|
|
25
|
+
* for real binaries.
|
|
26
|
+
*/
|
|
27
|
+
export function detectAgents(probeBin = probe) {
|
|
28
|
+
const found = [];
|
|
29
|
+
for (const kind of CANDIDATES) {
|
|
30
|
+
const result = probeBin(kind);
|
|
31
|
+
if (result)
|
|
32
|
+
found.push({ kind, version: result.version });
|
|
33
|
+
}
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface GitSnapshot {
|
|
2
|
+
head: string | null;
|
|
3
|
+
dirty: string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function isGitRepo(dir: string): boolean;
|
|
6
|
+
export declare function snapshot(dir: string): GitSnapshot;
|
|
7
|
+
export declare function diffSince(dir: string, snap: GitSnapshot): string;
|
|
8
|
+
export declare function changedFiles(dir: string, snap: GitSnapshot): string[];
|
|
9
|
+
/**
|
|
10
|
+
* Reverts the given files, which are expected to come from `changedFiles`
|
|
11
|
+
* (and therefore already exclude any untracked files that pre-date the
|
|
12
|
+
* snapshot).
|
|
13
|
+
*
|
|
14
|
+
* Tracked files are restored from the snapshot commit (`git checkout
|
|
15
|
+
* <snap.head> -- <file>`), NOT from the index — so a file that was `git add`ed
|
|
16
|
+
* after the snapshot is still restored to its snapshot content, not its staged
|
|
17
|
+
* (mutated) content. Files that are currently untracked (newly added since the
|
|
18
|
+
* snapshot) are deleted.
|
|
19
|
+
*
|
|
20
|
+
* If `snap.head` is null (a repo with no commits at snapshot time) there is no
|
|
21
|
+
* commit to restore tracked files from, so only the newly-added untracked files
|
|
22
|
+
* are deleted; any tracked-file changes are left in place.
|
|
23
|
+
*/
|
|
24
|
+
export declare function revert(dir: string, snap: GitSnapshot, files: string[]): void;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
function git(dir, args) {
|
|
5
|
+
return execFileSync('git', args, { cwd: dir, encoding: 'utf8' });
|
|
6
|
+
}
|
|
7
|
+
/** Untracked (non-ignored) files, relative to `dir`. */
|
|
8
|
+
function untrackedFiles(dir) {
|
|
9
|
+
const out = git(dir, ['ls-files', '--others', '--exclude-standard']);
|
|
10
|
+
return out.split('\n').filter((line) => line.length > 0);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Tracked files with uncommitted modifications (staged or unstaged), relative
|
|
14
|
+
* to `dir`. Diffs against the given commit; when `head` is null (a repo with no
|
|
15
|
+
* commits) there are no tracked-and-committed files, so returns [].
|
|
16
|
+
*/
|
|
17
|
+
function dirtyTrackedFiles(dir, head) {
|
|
18
|
+
if (!head)
|
|
19
|
+
return [];
|
|
20
|
+
const out = git(dir, ['diff', '--name-only', head]);
|
|
21
|
+
return out.split('\n').filter((line) => line.length > 0);
|
|
22
|
+
}
|
|
23
|
+
export function isGitRepo(dir) {
|
|
24
|
+
try {
|
|
25
|
+
const out = execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
26
|
+
cwd: dir,
|
|
27
|
+
encoding: 'utf8',
|
|
28
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
29
|
+
}).trim();
|
|
30
|
+
return out === 'true';
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function snapshot(dir) {
|
|
37
|
+
let head = null;
|
|
38
|
+
try {
|
|
39
|
+
head = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
40
|
+
cwd: dir,
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
43
|
+
}).trim();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
head = null;
|
|
47
|
+
}
|
|
48
|
+
const dirty = [...dirtyTrackedFiles(dir, head), ...untrackedFiles(dir)];
|
|
49
|
+
return { head, dirty };
|
|
50
|
+
}
|
|
51
|
+
export function diffSince(dir, snap) {
|
|
52
|
+
if (snap.head) {
|
|
53
|
+
return git(dir, ['diff', snap.head]);
|
|
54
|
+
}
|
|
55
|
+
return git(dir, ['diff']);
|
|
56
|
+
}
|
|
57
|
+
export function changedFiles(dir, snap) {
|
|
58
|
+
// Exclude any tracked file that was ALREADY dirty (uncommitted WIP) at
|
|
59
|
+
// snapshot time — mirrors the untracked filter below. We can't cleanly
|
|
60
|
+
// separate the user's pre-existing edits from the agent's own edits to the
|
|
61
|
+
// same file, so the safest choice is to leave such files out of
|
|
62
|
+
// `changedFiles` entirely: they won't be reported as agent changes, and
|
|
63
|
+
// (critically) `revert` won't touch them, preserving the user's WIP.
|
|
64
|
+
const trackedChanged = dirtyTrackedFiles(dir, snap.head).filter((f) => !snap.dirty.includes(f));
|
|
65
|
+
const newlyUntracked = untrackedFiles(dir).filter((f) => !snap.dirty.includes(f));
|
|
66
|
+
return [...new Set([...trackedChanged, ...newlyUntracked])];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Reverts the given files, which are expected to come from `changedFiles`
|
|
70
|
+
* (and therefore already exclude any untracked files that pre-date the
|
|
71
|
+
* snapshot).
|
|
72
|
+
*
|
|
73
|
+
* Tracked files are restored from the snapshot commit (`git checkout
|
|
74
|
+
* <snap.head> -- <file>`), NOT from the index — so a file that was `git add`ed
|
|
75
|
+
* after the snapshot is still restored to its snapshot content, not its staged
|
|
76
|
+
* (mutated) content. Files that are currently untracked (newly added since the
|
|
77
|
+
* snapshot) are deleted.
|
|
78
|
+
*
|
|
79
|
+
* If `snap.head` is null (a repo with no commits at snapshot time) there is no
|
|
80
|
+
* commit to restore tracked files from, so only the newly-added untracked files
|
|
81
|
+
* are deleted; any tracked-file changes are left in place.
|
|
82
|
+
*/
|
|
83
|
+
export function revert(dir, snap, files) {
|
|
84
|
+
if (files.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
const currentUntracked = new Set(untrackedFiles(dir));
|
|
87
|
+
const toDelete = files.filter((f) => currentUntracked.has(f));
|
|
88
|
+
const toCheckout = files.filter((f) => !currentUntracked.has(f));
|
|
89
|
+
for (const file of toDelete) {
|
|
90
|
+
rmSync(join(dir, file), { force: true });
|
|
91
|
+
}
|
|
92
|
+
if (toCheckout.length > 0 && snap.head) {
|
|
93
|
+
git(dir, ['checkout', snap.head, '--', ...toCheckout]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AgentKind } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* When an agent CLI exits nonzero without ever emitting a terminal `done`,
|
|
4
|
+
* inspect its buffered stderr tail for known model-rejection shapes (a stale
|
|
5
|
+
* CLI that doesn't recognize the requested model). Returns an actionable
|
|
6
|
+
* hint to append to the error event text, or undefined when nothing matches.
|
|
7
|
+
*/
|
|
8
|
+
export declare function modelRejectionHint(kind: AgentKind, stderrTail: string): string | undefined;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const MODEL_REJECTION_RE = /unknown model|unsupported model|invalid model|model.*not.*(found|supported)/i;
|
|
2
|
+
/**
|
|
3
|
+
* When an agent CLI exits nonzero without ever emitting a terminal `done`,
|
|
4
|
+
* inspect its buffered stderr tail for known model-rejection shapes (a stale
|
|
5
|
+
* CLI that doesn't recognize the requested model). Returns an actionable
|
|
6
|
+
* hint to append to the error event text, or undefined when nothing matches.
|
|
7
|
+
*/
|
|
8
|
+
export function modelRejectionHint(kind, stderrTail) {
|
|
9
|
+
if (!MODEL_REJECTION_RE.test(stderrTail))
|
|
10
|
+
return undefined;
|
|
11
|
+
return `hint: your ${kind} CLI may be too old for the selected model — upgrade it or switch backend in Settings.`;
|
|
12
|
+
}
|