diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc index 4781c437a76ca..3a9eb2bb29fd0 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc @@ -34,8 +34,8 @@ over the MCP protocol. == Available Tools -The server exposes 30 tools organized into twelve functional areas, plus 3 prompts that provide structured -multi-step workflows. +The server exposes 30 catalog tools organized into twelve functional areas, 21 runtime introspection tools for +inspecting and interacting with live Camel processes, plus 3 prompts that provide structured multi-step workflows. === Catalog Exploration @@ -264,6 +264,112 @@ help validate, scaffold, and provide mock guidance for that workflow. | Lists available Camel versions for a given runtime, including release dates, JDK requirements, and LTS status. |=== +=== Runtime Introspection + +NOTE: Runtime tools require a running Camel application started via `camel run`. They communicate with the +application through the file-based IPC protocol in `~/.camel/`. All tools accept an optional `nameOrPid` +parameter; when omitted, the server auto-discovers the running Camel process (this works when exactly one +process is running). + +==== Process Discovery + +[cols="1,3",options="header"] +|=== +| Tool | Description + +| `camel_runtime_processes` +| List all running Camel processes that can be inspected. Returns PID, name, and context name for each + discovered process. +|=== + +==== Context and Routes + +[cols="1,3",options="header"] +|=== +| Tool | Description + +| `camel_runtime_context` +| Get Camel context information: name, version, state, uptime, route count, exchange statistics. + +| `camel_runtime_routes` +| List Camel routes with their state, uptime, messages processed, last error, and throughput statistics. + +| `camel_runtime_route_source` +| Get the source code of routes in the running Camel application. Supports wildcard filtering. + +| `camel_runtime_route_dump` +| Dump route definitions in XML or YAML format. + +| `camel_runtime_route_structure` +| Show the route structure as a tree of processors. + +| `camel_runtime_route_control` +| Control a route: start, stop, suspend, or resume it by route ID. +|=== + +==== Observability + +[cols="1,3",options="header"] +|=== +| Tool | Description + +| `camel_runtime_health` +| Get health check status for the Camel application. + +| `camel_runtime_endpoints` +| List all endpoints registered in the Camel context with their URIs and usage statistics. + +| `camel_runtime_inflight` +| Show currently in-flight exchanges (messages being processed). + +| `camel_runtime_blocked` +| Show blocked exchanges that are stuck or waiting. + +| `camel_runtime_top` +| Show top processor statistics: which processors are slowest and most active. + +| `camel_runtime_memory` +| Show JVM memory usage (heap/non-heap), garbage collection stats, and thread counts. +|=== + +==== Configuration and Registry + +[cols="1,3",options="header"] +|=== +| Tool | Description + +| `camel_runtime_variables` +| Show exchange variables in the Camel context. + +| `camel_runtime_consumers` +| Show consumer statistics (polling consumers, event-driven consumers). + +| `camel_runtime_properties` +| Show configuration properties of the running Camel application. + +| `camel_runtime_services` +| Show services registered in the Camel service registry. +|=== + +==== Interaction and Debugging + +[cols="1,3",options="header"] +|=== +| Tool | Description + +| `camel_runtime_send` +| Send a test message to a Camel endpoint in the running application. + +| `camel_runtime_trace` +| Enable, disable, or dump message tracing for the running Camel application. + +| `camel_runtime_eval` +| Evaluate an expression in the given language (e.g., simple, jsonpath, xpath) against the Camel context. + +| `camel_runtime_browse` +| Browse messages in a Camel endpoint (e.g., messages queued in a SEDA endpoint). +|=== + == Available Prompts Prompts are structured multi-step workflows that guide the LLM through orchestrating multiple tools in the correct @@ -445,6 +551,109 @@ Once connected, you can navigate the available MCP Tools, Resources, and Prompts This is useful for understanding what data the server provides and for testing tool invocations before integrating with an AI coding assistant. +== Getting Started + +This walkthrough demonstrates the full AI-assisted Camel workflow: use the catalog tools to build a route, +launch it with `camel run`, then use the runtime tools to inspect and interact with the live application. +The same MCP server provides both catalog and runtime capabilities. + +=== Prerequisites + +* https://www.jbang.dev/[JBang] installed and on your PATH +* https://camel.apache.org/manual/camel-jbang.html[Camel JBang] installed (`jbang app install camel@apache/camel`) +* An MCP-capable AI tool (Claude Code, VS Code with Copilot, JetBrains AI, etc.) configured as shown in the <> section above + +=== Step 1: Ask the AI to build a route + +Start by asking your AI assistant to create a Camel route. The assistant uses the catalog tools to discover +components, look up their documentation, build the route, and validate it: + +---- +Build me a Camel route that generates a message every 5 seconds with a random number, +logs it, and sends it to a SEDA queue called "numbers". +---- + +The assistant uses `camel_catalog_component_doc` to look up the `timer`, `log`, and `seda` component options, +builds a YAML route, and validates it with `camel_validate_yaml_dsl`. You can also use the +`camel_build_integration` prompt for a more structured multi-step workflow. + +The result is a route file, for example `random-numbers.yaml`: + +[source,yaml] +---- +- route: + from: + uri: timer:generate?period=5000 + steps: + - setBody: + simple: "${random(1000)}" + - log: "Generated number: ${body}" + - to: seda:numbers +---- + +=== Step 2: Launch the route + +In AI tools that can execute shell commands (such as Claude Code), ask the assistant to launch the route: + +---- +Run this route with camel run +---- + +The assistant runs `camel run random-numbers.yaml` in the background. Alternatively, open a separate terminal +and run it yourself: + +[source,bash] +---- +camel run random-numbers.yaml +---- + +=== Step 3: Inspect the running application + +Once the route is running, use the runtime tools to inspect it: + +---- +Show me the running Camel processes and their route statistics +---- + +The assistant calls `camel_runtime_processes` to discover the running application, then `camel_runtime_routes` +to show route state, message counts, and throughput. You can drill deeper: + +---- +Are there any health issues? What do the endpoints look like? +---- + +The assistant calls `camel_runtime_health` and `camel_runtime_endpoints` to provide a full picture of the +live application. + +=== Step 4: Interact with the running application + +Try sending a test message or enabling tracing: + +---- +Enable message tracing so I can see the exchange flow, wait a few seconds, +then show me the traced messages. +---- + +The assistant calls `camel_runtime_trace` with `action=enable`, waits, then calls it again with `action=dump` +to show the message path through the route processors. + +---- +Browse the messages queued in the seda:numbers endpoint +---- + +The assistant calls `camel_runtime_browse` with `endpoint=seda:numbers` to show pending messages. + +=== Step 5: Debug and iterate + +If something goes wrong, combine catalog and runtime tools: + +---- +The route is showing errors. Diagnose the issue and suggest a fix. +---- + +The assistant calls `camel_runtime_health` and checks error details from `camel_runtime_routes`, then uses +`camel_error_diagnose` (a catalog tool) to analyze the error and suggest fixes — all in one conversation. + == Examples === Listing Components @@ -738,3 +947,60 @@ Format: yaml The prompt guides the assistant through a security audit: analyzing the route for vulnerabilities and security-sensitive components, understanding the data flow, and producing a structured audit checklist with critical issues, warnings, positive findings, and actionable recommendations. + +=== Runtime Introspection Examples + +The runtime tools require a Camel application to be running. Start one with `camel run`: + +[source,bash] +---- +camel run my-route.yaml +---- + +Then, from a separate terminal (or through your AI assistant), the MCP server auto-discovers the running process. + +==== Listing Running Processes + +---- +Show me the running Camel processes +---- + +The assistant calls `camel_runtime_processes` and returns the PID, name, and context name for each discovered +Camel application. + +==== Inspecting Context and Routes + +---- +Show me the Camel context info and route statistics for the running application +---- + +The assistant calls `camel_runtime_context` to get context metadata (version, state, uptime) and +`camel_runtime_routes` to list routes with their state, exchange counts, and throughput. + +==== Debugging a Live Route + +---- +Enable tracing so I can see messages flowing through my routes, then show me the traced messages +---- + +The assistant calls `camel_runtime_trace` with `action=enable` to start tracing, waits briefly, then calls +`camel_runtime_trace` with `action=dump` to retrieve the traced messages showing the path each exchange took +through the route processors. + +==== Sending a Test Message + +---- +Send a test message with body "Hello" to the direct:start endpoint +---- + +The assistant calls `camel_runtime_send` with `endpoint=direct:start` and `body=Hello`. The tool returns the +exchange result and any response. + +==== Finding Performance Bottlenecks + +---- +Which processors in my routes are the slowest? +---- + +The assistant calls `camel_runtime_top` to get processor-level statistics sorted by processing time, +helping identify bottlenecks in the running application. diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-ask.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-ask.adoc new file mode 100644 index 0000000000000..0710cd347ae84 --- /dev/null +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-ask.adoc @@ -0,0 +1,34 @@ + +// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE += camel ask + +Ask a question about a running Camel application using AI + + +== Usage + +[source,bash] +---- +camel ask [options] +---- + + + +== Options + +[cols="2,5,1,2",options="header"] +|=== +| Option | Description | Default | Type +| `--api-key` | API key. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY env vars | | String +| `--api-type` | API type: 'ollama', 'openai', or 'anthropic' | | ApiType +| `--max-iterations` | Maximum number of tool-calling rounds | 10 | int +| `--model` | Model to use | DEFAULT_MODEL | String +| `--name` | Name or PID of the Camel process. Auto-detected when exactly one process is running | | String +| `--show-tools` | Show tool calls and results as they happen | | boolean +| `--timeout` | Timeout in seconds for LLM response | 120 | int +| `--url` | LLM API endpoint URL. Auto-detected if not specified. | | String +| `--verbose` | Print debug information: HTTP requests, responses, and parsed results | | boolean +| `-h,--help` | Display the help and sub-commands | | boolean +|=== + + diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc index 5425cf41a8360..48a42450ea204 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc @@ -11,6 +11,7 @@ TIP: You can also use `camel --help` or `camel --help` to see availabl [cols="2,5",options="header"] |=== | Command | Description +| xref:jbang-commands/camel-jbang-ask.adoc[camel ask] | Ask a question about a running Camel application using AI | xref:jbang-commands/camel-jbang-bind.adoc[camel bind] | DEPRECATED: Bind source and sink Kamelets as a new Camel integration | xref:jbang-commands/camel-jbang-catalog.adoc[camel catalog] | List artifacts from Camel Catalog | xref:jbang-commands/camel-jbang-cmd.adoc[camel cmd] | Performs commands in the running Camel integrations, such as start/stop route, or change logging levels. diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-explain.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-explain.adoc index b87ed60b086e6..c856557ff5c33 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-explain.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-explain.adoc @@ -19,8 +19,8 @@ camel explain [options] [cols="2,5,1,2",options="header"] |=== | Option | Description | Default | Type -| `--api-key` | API key for authentication. Also reads OPENAI_API_KEY or LLM_API_KEY env vars | | String -| `--api-type` | API type: 'ollama' or 'openai' (OpenAI-compatible) | ollama | ApiType +| `--api-key` | API key for authentication. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY env vars | | String +| `--api-type` | API type: 'ollama', 'openai' (OpenAI-compatible), or 'anthropic' (Anthropic/Vertex AI) | | ApiType | `--catalog-context` | Include Camel Catalog descriptions in the prompt | | boolean | `--format` | Output format (text, markdown) | text | String | `--model` | Model to use | DEFAULT_MODEL | String diff --git a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java index 3f5f0c10b7115..bb5b0506492d4 100644 --- a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java +++ b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java @@ -272,12 +272,36 @@ protected void task() { } protected void actionTask() { + // scan for all action files: {pid}-action.json (legacy) and {pid}-action-{requestId}.json (multi-client) + File dir = lockFile.getParentFile(); + String prefix = lockFile.getName() + "-action"; + File[] actionFiles = dir.listFiles((d, name) -> name.startsWith(prefix) && name.endsWith(".json")); + if (actionFiles == null || actionFiles.length == 0) { + return; + } + for (File af : actionFiles) { + String suffix = af.getName().substring(prefix.length()); + // suffix is either ".json" (legacy) or "-{requestId}.json" (multi-client) + String requestId = suffix.startsWith("-") + ? suffix.substring(1, suffix.length() - 5) // strip leading "-" and trailing ".json" + : null; + File of = requestId != null + ? new File(dir, lockFile.getName() + "-output-" + requestId + ".json") + : this.outputFile; + processAction(af, of); + } + } + + private void processAction(File af, File of) { String action = null; + File prevOutputFile = this.outputFile; try { - JsonObject root = loadAction(); + JsonObject root = loadAction(af); if (root == null || root.isEmpty()) { return; } + // set outputFile so all doAction* methods write to the correct file + this.outputFile = of; if (LOG.isDebugEnabled()) { LOG.debug("Action: {}", root); @@ -342,13 +366,12 @@ protected void actionTask() { doActionCliDebug(root); } } catch (Exception e) { - // ignore - LOG.warn("Error executing action: {} due to: {}. This exception is ignored.", action != null ? action : actionFile, + LOG.warn("Error executing action: {} due to: {}. This exception is ignored.", action != null ? action : af, e.getMessage(), e); } finally { - // action done so delete file - FileUtil.deleteFile(actionFile); + this.outputFile = prevOutputFile; + FileUtil.deleteFile(af); } } @@ -1116,9 +1139,13 @@ private void doActionProcessorTask(JsonObject root) { } JsonObject loadAction() { + return loadAction(actionFile); + } + + JsonObject loadAction(File file) { try { - if (actionFile != null && actionFile.exists()) { - FileInputStream fis = new FileInputStream(actionFile); + if (file != null && file.exists()) { + FileInputStream fis = new FileInputStream(file); String text = IOHelper.loadText(fis); IOHelper.close(fis); if (!text.isEmpty()) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json index 689a7bf27e851..fdc22c91dc6cc 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json +++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json @@ -1,5 +1,6 @@ { "commands": [ + { "name": "ask", "fullName": "ask", "description": "Ask a question about a running Camel application using AI", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": "--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama', 'openai', or 'anthropic'", "javaType": "LlmClient.ApiType", "type": "object" }, { "names": "--max-iterations", "description": "Maximum number of tool-calling rounds", "defaultValue": "10", "javaType": "int", "type": "integer" }, { "names": "--model", "description": "Model to use", "defaultValue": "DEFAULT_MODEL", "javaType": "java.lang.String", "type": "string" }, { "names": "--name", "description": "Name or PID of the Camel process. Auto-detected when exactly one process is running", "javaType": "java.lang.String", "type": "string" }, { "names": "--show-tools", "description": "Show tool calls and results as they happen", "javaType": "boolean", "type": "boolean" }, { "names": "--timeout", "description": "Timeout in seconds for LLM response", "defaultValue": "120", "javaType": "int", "type": "integer" }, { "names": "--url", "description": "LLM API endpoint URL. Auto-detected if not specified.", "javaType": "java.lang.String", "type": "string" }, { "names": "--verbose", "description": "Print debug information: HTTP requests, responses, and parsed results", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind source and sink Kamelets as a new Camel integration", "deprecated": true, "sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": [ { "names": "--error-handler", "description": "Add error handler (none|log|sink:). Sink endpoints are expected in the format [[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet name.", "javaType": "java.lang.String", "type": "string" }, { "names": "--output", "description": "Output format generated by this command (supports: file, yaml, json).", "defaultValue": "file", "javaType": "java.lang.String", "type": "string" }, { "names": "--property", "description": "Adds a pipe property in the form of [source|sink|error-handler|step-].= where is the step number starting from 1", "javaType": "java.lang.String", "type": "string" }, { "names": "--sink", "description": "Sink (to) such as a Kamelet or Camel endpoint uri", "javaType": "java.lang.String", "type": "string", "required": true }, { "names": "--source", "description": "Source (from) such as a Kamelet or Camel endpoint uri", "javaType": "java.lang.String", "type": "string", "required": true }, { "names": "--step", "description": "Optional steps such as a Kamelet or Camel endpoint uri", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "catalog", "fullName": "catalog", "description": "List artifacts from Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "component", "fullName": "catalog component", "description": "List components from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogComponent", "options": [ { "names": "--camel-version", "description": "To use a different Camel version than the default version", "javaType": "java.lang.String", "type": "string" }, { "names": "--display-gav", "description": "Display Maven GAV instead of name", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--download", "description": "Whether to allow automatic downloading JAR dependencies (over the internet)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--filter", "description": "Filter by name or description", "javaType": "java.lang.String", "type": "string" }, { "names": "--fresh", "description": "Make sure we use fresh (i.e. non-cached) resources", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--quarkus-artifact-id", "description": "Deprecated. This value is not used anymore. It is kept only for backwards compatibility and will be removed in Camel 5.x. Camel commands may use either 'quarkus-bom' or 'quarkus-camel-bom' artifactIds depending on the context.", "defaultValue": "quarkus-bom", "javaType": "java.lang.String", "type": "string", "deprecated": true }, { "names": "--quarkus-ext-registry", "description": "The base URI of Quarkus Extension Registry. The default is {@value RuntimeType#QUARKUS_EXTENSION_REGISTRY_BASE_URL} unless camel.jbang.quarkus.platform.url system property is set (the \/client\/platforms suffix is removed if present).", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-group-id", "description": "groupId of Quarkus Platform BOM; honored only if --quarkus-version is set", "defaultValue": "io.quarkus.platform", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-version", "description": "version of Quarkus Platform BOM; the default value is looked up in Quarkus Extension Registry", "javaType": "java.lang.String", "type": "string" }, { "names": "--repo,--repos", "description": "Additional maven repositories for download on-demand (Use commas to separate multiple repositories)", "javaType": "java.lang.String", "type": "string" }, { "names": "--runtime", "description": "Runtime (camel-main, spring-boot, quarkus)", "javaType": "org.apache.camel.dsl.jbang.core.common.RuntimeType", "type": "object" }, { "names": "--since-after", "description": "Filter by version more recent (inclusive)", "javaType": "java.lang.String", "type": "string" }, { "names": "--since-before", "description": "Filter by version older (inclusive)", "javaType": "java.lang.String", "type": "string" }, { "names": "--sort", "description": "Sort by name, support-level, or description", "defaultValue": "name", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "dataformat", "fullName": "catalog dataformat", "description": "List data formats from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDataFormat", "options": [ { "names": "--camel-version", "description": "To use a different Camel version than the default version", "javaType": "java.lang.String", "type": "string" }, { "names": "--display-gav", "description": "Display Maven GAV instead of name", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--download", "description": "Whether to allow automatic downloading JAR dependencies (over the internet)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--filter", "description": "Filter by name or description", "javaType": "java.lang.String", "type": "string" }, { "names": "--fresh", "description": "Make sure we use fresh (i.e. non-cached) resources", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--quarkus-artifact-id", "description": "Deprecated. This value is not used anymore. It is kept only for backwards compatibility and will be removed in Camel 5.x. Camel commands may use either 'quarkus-bom' or 'quarkus-camel-bom' artifactIds depending on the context.", "defaultValue": "quarkus-bom", "javaType": "java.lang.String", "type": "string", "deprecated": true }, { "names": "--quarkus-ext-registry", "description": "The base URI of Quarkus Extension Registry. The default is {@value RuntimeType#QUARKUS_EXTENSION_REGISTRY_BASE_URL} unless camel.jbang.quarkus.platform.url system property is set (the \/client\/platforms suffix is removed if present).", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-group-id", "description": "groupId of Quarkus Platform BOM; honored only if --quarkus-version is set", "defaultValue": "io.quarkus.platform", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-version", "description": "version of Quarkus Platform BOM; the default value is looked up in Quarkus Extension Registry", "javaType": "java.lang.String", "type": "string" }, { "names": "--repo,--repos", "description": "Additional maven repositories for download on-demand (Use commas to separate multiple repositories)", "javaType": "java.lang.String", "type": "string" }, { "names": "--runtime", "description": "Runtime (camel-main, spring-boot, quarkus)", "javaType": "org.apache.camel.dsl.jbang.core.common.RuntimeType", "type": "object" }, { "names": "--since-after", "description": "Filter by version more recent (inclusive)", "javaType": "java.lang.String", "type": "string" }, { "names": "--since-before", "description": "Filter by version older (inclusive)", "javaType": "java.lang.String", "type": "string" }, { "names": "--sort", "description": "Sort by name, support-level, or description", "defaultValue": "name", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "dev-console", "fullName": "catalog dev-console", "description": "List dev-consoles from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDevConsole", "options": [ { "names": "--camel-version", "description": "To use a different Camel version than the default version", "javaType": "java.lang.String", "type": "string" }, { "names": "--display-gav", "description": "Display Maven GAV instead of name", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--download", "description": "Whether to allow automatic downloading JAR dependencies (over the internet)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--filter", "description": "Filter by name or description", "javaType": "java.lang.String", "type": "string" }, { "names": "--fresh", "description": "Make sure we use fresh (i.e. non-cached) resources", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--quarkus-artifact-id", "description": "Deprecated. This value is not used anymore. It is kept only for backwards compatibility and will be removed in Camel 5.x. Camel commands may use either 'quarkus-bom' or 'quarkus-camel-bom' artifactIds depending on the context.", "defaultValue": "quarkus-bom", "javaType": "java.lang.String", "type": "string", "deprecated": true }, { "names": "--quarkus-ext-registry", "description": "The base URI of Quarkus Extension Registry. The default is {@value RuntimeType#QUARKUS_EXTENSION_REGISTRY_BASE_URL} unless camel.jbang.quarkus.platform.url system property is set (the \/client\/platforms suffix is removed if present).", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-group-id", "description": "groupId of Quarkus Platform BOM; honored only if --quarkus-version is set", "defaultValue": "io.quarkus.platform", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-version", "description": "version of Quarkus Platform BOM; the default value is looked up in Quarkus Extension Registry", "javaType": "java.lang.String", "type": "string" }, { "names": "--repo,--repos", "description": "Additional maven repositories for download on-demand (Use commas to separate multiple repositories)", "javaType": "java.lang.String", "type": "string" }, { "names": "--runtime", "description": "Runtime (camel-main, spring-boot, quarkus)", "javaType": "org.apache.camel.dsl.jbang.core.common.RuntimeType", "type": "object" }, { "names": "--since-after", "description": "Filter by version more recent (inclusive)", "javaType": "java.lang.String", "type": "string" }, { "names": "--since-before", "description": "Filter by version older (inclusive)", "javaType": "java.lang.String", "type": "string" }, { "names": "--sort", "description": "Sort by name, support-level, or description", "defaultValue": "name", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "kamelet", "fullName": "catalog kamelet", "description": "List Kamelets from the Kamelet Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogKamelet", "options": [ { "names": "--filter", "description": "Filter by name or description", "javaType": "java.lang.String", "type": "string" }, { "names": "--kamelets-version", "description": "Apache Camel Kamelets version", "defaultValue": "RuntimeType.KAMELETS_VERSION", "javaType": "java.lang.String", "type": "string" }, { "names": "--sort", "description": "Sort by name, type, support-level, or description", "defaultValue": "name", "javaType": "java.lang.String", "type": "string" }, { "names": "--type,--filter-type", "description": "Filter by type: source, sink, or action", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "language", "fullName": "catalog language", "description": "List expression languages from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogLanguage", "options": [ { "names": "--camel-version", "description": "To use a different Camel version than the default version", "javaType": "java.lang.String", "type": "string" }, { "names": "--display-gav", "description": "Display Maven GAV instead of name", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--download", "description": "Whether to allow automatic downloading JAR dependencies (over the internet)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--filter", "description": "Filter by name or description", "javaType": "java.lang.String", "type": "string" }, { "names": "--fresh", "description": "Make sure we use fresh (i.e. non-cached) resources", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--quarkus-artifact-id", "description": "Deprecated. This value is not used anymore. It is kept only for backwards compatibility and will be removed in Camel 5.x. Camel commands may use either 'quarkus-bom' or 'quarkus-camel-bom' artifactIds depending on the context.", "defaultValue": "quarkus-bom", "javaType": "java.lang.String", "type": "string", "deprecated": true }, { "names": "--quarkus-ext-registry", "description": "The base URI of Quarkus Extension Registry. The default is {@value RuntimeType#QUARKUS_EXTENSION_REGISTRY_BASE_URL} unless camel.jbang.quarkus.platform.url system property is set (the \/client\/platforms suffix is removed if present).", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-group-id", "description": "groupId of Quarkus Platform BOM; honored only if --quarkus-version is set", "defaultValue": "io.quarkus.platform", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-version", "description": "version of Quarkus Platform BOM; the default value is looked up in Quarkus Extension Registry", "javaType": "java.lang.String", "type": "string" }, { "names": "--repo,--repos", "description": "Additional maven repositories for download on-demand (Use commas to separate multiple repositories)", "javaType": "java.lang.String", "type": "string" }, { "names": "--runtime", "description": "Runtime (camel-main, spring-boot, quarkus)", "javaType": "org.apache.camel.dsl.jbang.core.common.RuntimeType", "type": "object" }, { "names": "--since-after", "description": "Filter by version more recent (inclusive)", "javaType": "java.lang.String", "type": "string" }, { "names": "--since-before", "description": "Filter by version older (inclusive)", "javaType": "java.lang.String", "type": "string" }, { "names": "--sort", "description": "Sort by name, support-level, or description", "defaultValue": "name", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "other", "fullName": "catalog other", "description": "List miscellaneous components from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogOther", "options": [ { "names": "--camel-version", "description": "To use a different Camel version than the default version", "javaType": "java.lang.String", "type": "string" }, { "names": "--display-gav", "description": "Display Maven GAV instead of name", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--download", "description": "Whether to allow automatic downloading JAR dependencies (over the internet)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--filter", "description": "Filter by name or description", "javaType": "java.lang.String", "type": "string" }, { "names": "--fresh", "description": "Make sure we use fresh (i.e. non-cached) resources", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--quarkus-artifact-id", "description": "Deprecated. This value is not used anymore. It is kept only for backwards compatibility and will be removed in Camel 5.x. Camel commands may use either 'quarkus-bom' or 'quarkus-camel-bom' artifactIds depending on the context.", "defaultValue": "quarkus-bom", "javaType": "java.lang.String", "type": "string", "deprecated": true }, { "names": "--quarkus-ext-registry", "description": "The base URI of Quarkus Extension Registry. The default is {@value RuntimeType#QUARKUS_EXTENSION_REGISTRY_BASE_URL} unless camel.jbang.quarkus.platform.url system property is set (the \/client\/platforms suffix is removed if present).", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-group-id", "description": "groupId of Quarkus Platform BOM; honored only if --quarkus-version is set", "defaultValue": "io.quarkus.platform", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-version", "description": "version of Quarkus Platform BOM; the default value is looked up in Quarkus Extension Registry", "javaType": "java.lang.String", "type": "string" }, { "names": "--repo,--repos", "description": "Additional maven repositories for download on-demand (Use commas to separate multiple repositories)", "javaType": "java.lang.String", "type": "string" }, { "names": "--runtime", "description": "Runtime (camel-main, spring-boot, quarkus)", "javaType": "org.apache.camel.dsl.jbang.core.common.RuntimeType", "type": "object" }, { "names": "--since-after", "description": "Filter by version more recent (inclusive)", "javaType": "java.lang.String", "type": "string" }, { "names": "--since-before", "description": "Filter by version older (inclusive)", "javaType": "java.lang.String", "type": "string" }, { "names": "--sort", "description": "Sort by name, support-level, or description", "defaultValue": "name", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "transformer", "fullName": "catalog transformer", "description": "List data type transformers from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogTransformer", "options": [ { "names": "--camel-version", "description": "To use a different Camel version than the default version", "javaType": "java.lang.String", "type": "string" }, { "names": "--display-gav", "description": "Display Maven GAV instead of name", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--download", "description": "Whether to allow automatic downloading JAR dependencies (over the internet)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--filter", "description": "Filter by name or description", "javaType": "java.lang.String", "type": "string" }, { "names": "--fresh", "description": "Make sure we use fresh (i.e. non-cached) resources", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--quarkus-artifact-id", "description": "Deprecated. This value is not used anymore. It is kept only for backwards compatibility and will be removed in Camel 5.x. Camel commands may use either 'quarkus-bom' or 'quarkus-camel-bom' artifactIds depending on the context.", "defaultValue": "quarkus-bom", "javaType": "java.lang.String", "type": "string", "deprecated": true }, { "names": "--quarkus-ext-registry", "description": "The base URI of Quarkus Extension Registry. The default is {@value RuntimeType#QUARKUS_EXTENSION_REGISTRY_BASE_URL} unless camel.jbang.quarkus.platform.url system property is set (the \/client\/platforms suffix is removed if present).", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-group-id", "description": "groupId of Quarkus Platform BOM; honored only if --quarkus-version is set", "defaultValue": "io.quarkus.platform", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-version", "description": "version of Quarkus Platform BOM; the default value is looked up in Quarkus Extension Registry", "javaType": "java.lang.String", "type": "string" }, { "names": "--repo,--repos", "description": "Additional maven repositories for download on-demand (Use commas to separate multiple repositories)", "javaType": "java.lang.String", "type": "string" }, { "names": "--runtime", "description": "Runtime (camel-main, spring-boot, quarkus)", "javaType": "org.apache.camel.dsl.jbang.core.common.RuntimeType", "type": "object" }, { "names": "--since-after", "description": "Filter by version more recent (inclusive)", "javaType": "java.lang.String", "type": "string" }, { "names": "--since-before", "description": "Filter by version older (inclusive)", "javaType": "java.lang.String", "type": "string" }, { "names": "--sort", "description": "Sort by name, support-level, or description", "defaultValue": "name", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] } ] }, { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelBrowseAction", "options": [ { "names": "--body-max-chars", "description": "Maximum size of the message body to include in the dump", "javaType": "int", "type": "integer" }, { "names": "--dump", "description": "Whether to include message dumps", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--endpoint", "description": "Endpoint to browse messages from (can be uri, pattern, or refer to a route id)", "javaType": "java.lang.String", "type": "string" }, { "names": "--fresh-size", "description": "Whether to calculate fresh queue size information (performance overhead)", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--limit", "description": "Limits the number of messages to dump per endpoint", "defaultValue": "100", "javaType": "int", "type": "integer" }, { "names": "--logging-color", "description": "Use colored logging", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--mask", "description": "Whether to mask endpoint URIs to avoid printing sensitive information such as password or access keys", "javaType": "boolean", "type": "boolean" }, { "names": "--only-body", "description": "Show only message body in browsed messages", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--pretty", "description": "Pretty print message body when using JSon or XML format", "javaType": "boolean", "type": "boolean" }, { "names": "--short-uri", "description": "List endpoint URI without query parameters (short)", "javaType": "boolean", "type": "boolean" }, { "names": "--show-body", "description": "Show message body in browsed messages", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--show-headers", "description": "Show message headers in browsed messages", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by uri, or size", "defaultValue": "uri", "javaType": "java.lang.String", "type": "string" }, { "names": "--tail", "description": "The number of messages from the end (latest) to dump", "javaType": "int", "type": "integer" }, { "names": "--wide-uri", "description": "List endpoint URI in full details", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "disable-processor", "fullName": "cmd disable-processor", "description": "Disable Camel processor", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelProcessorDisableAction", "options": [ { "names": "--id", "description": "Processor ids (multiple ids can be separated by comma)", "defaultValue": "*", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "enable-processor", "fullName": "cmd enable-processor", "description": "Enable Camel processor", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelProcessorEnableAction", "options": [ { "names": "--id", "description": "Processor ids (multiple ids can be separated by comma)", "defaultValue": "*", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "gc", "fullName": "cmd gc", "description": "Trigger Java Memory Garbage Collector", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelGCAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "load", "fullName": "cmd load", "description": "Loads new source files into an existing Camel", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelLoadAction", "options": [ { "names": "--restart", "description": "To force restart all routes after loading source files", "javaType": "boolean", "type": "boolean" }, { "names": "--source", "description": "Source file(s) to load", "javaType": "java.util.List", "type": "array" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "logger", "fullName": "cmd logger", "description": "List or change logging levels", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.LoggerAction", "options": [ { "names": "--logger", "description": "The logger name", "defaultValue": "root", "javaType": "java.lang.String", "type": "string" }, { "names": "--logging-level", "description": "To change logging level (ERROR, WARN, INFO, DEBUG, TRACE)", "javaType": "java.lang.String", "type": "string" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "receive", "fullName": "cmd receive", "description": "Receive and dump messages from remote endpoints", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelReceiveAction", "options": [ { "names": "--action", "description": "Action to start, stop, clear, status, or dump messages", "defaultValue": "status", "javaType": "java.lang.String", "type": "string" }, { "names": "--compact", "description": "Compact output (no empty line separating messages)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--download", "description": "Whether to allow automatic downloading JAR dependencies (over the internet)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--endpoint,--uri", "description": "Endpoint to receive messages from (can be uri or pattern to refer to existing endpoint)", "javaType": "java.lang.String", "type": "string" }, { "names": "--find", "description": "Find and highlight matching text (ignore case).", "javaType": "java.lang.String", "type": "string" }, { "names": "--follow", "description": "Keep following and outputting new messages (press enter to exit).", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--fresh", "description": "Make sure we use fresh (i.e. non-cached) resources", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--grep", "description": "Filter messages to only output matching text (ignore case).", "javaType": "java.lang.String", "type": "string" }, { "names": "--logging-color", "description": "Use colored logging", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--mask", "description": "Whether to mask endpoint URIs to avoid printing sensitive information such as password or access keys", "javaType": "boolean", "type": "boolean" }, { "names": "--only-body", "description": "Show only message body in received messages", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--output", "description": "Output format (auto, true, false)", "javaType": "java.lang.String", "type": "string" }, { "names": "--prefix", "description": "Print prefix with running Camel integration name. auto=only prefix when running multiple integrations. true=always prefix. false=prefix off.", "defaultValue": "auto", "javaType": "java.lang.String", "type": "string" }, { "names": "--pretty", "description": "Pretty print message body when using JSon or XML format", "javaType": "boolean", "type": "boolean" }, { "names": "--prop,--property", "description": "Additional properties; override existing (only applicable when NOT using an existing running Camel)", "javaType": "java.lang.String", "type": "string" }, { "names": "--properties", "description": "comma separated list of properties file (only applicable when NOT using an existing running Camel) (ex. \/path\/to\/file.properties,\/path\/to\/other.properties", "javaType": "java.lang.String", "type": "string" }, { "names": "--repo,--repos", "description": "Additional maven repositories for download on-demand (Use commas to separate multiple repositories)", "javaType": "java.lang.String", "type": "string" }, { "names": "--short-uri", "description": "List endpoint URI without query parameters (short)", "javaType": "boolean", "type": "boolean" }, { "names": "--show-body", "description": "Show message body in received messages", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--show-exchange-properties", "description": "Show exchange properties in received messages", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--show-exchange-variables", "description": "Show exchange variables in received messages", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--show-headers", "description": "Show message headers in received messages", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--since", "description": "Return messages newer than a relative duration like 5s, 2m, or 1h. The value is in seconds if no unit specified.", "javaType": "java.lang.String", "type": "string" }, { "names": "--sort", "description": "Sort by pid, name or age for showing status of messages", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--tail", "description": "The number of messages from the end to show. Use -1 to read from the beginning. Use 0 to read only new lines. Defaults to showing all messages from beginning.", "defaultValue": "-1", "javaType": "int", "type": "integer" }, { "names": "--timeout", "description": "Timeout in millis waiting for message to be received", "defaultValue": "20000", "javaType": "long", "type": "integer" }, { "names": "--wide-uri", "description": "List endpoint URI in full details", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "reload", "fullName": "cmd reload", "description": "Trigger reloading Camel", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelReloadAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "reset-stats", "fullName": "cmd reset-stats", "description": "Reset performance statistics", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelResetStatsAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "resume-route", "fullName": "cmd resume-route", "description": "Resume Camel routes", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelRouteResumeAction", "options": [ { "names": "--id", "description": "Route ids (multiple ids can be separated by comma)", "defaultValue": "*", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "route-diagram", "fullName": "cmd route-diagram", "description": "Display Camel route diagram in the terminal", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelRouteDiagramAction", "options": [ { "names": "--box-width", "description": "Node box width in logical pixels", "defaultValue": "180", "javaType": "int", "type": "integer" }, { "names": "--filter", "description": "Filter route by filename or route id", "javaType": "java.lang.String", "type": "string" }, { "names": "--font-size", "description": "Font size in logical pixels for node text", "defaultValue": "12", "javaType": "int", "type": "integer" }, { "names": "--ignore-loading-error", "description": "Whether to ignore route loading and compilation errors (use this with care!)", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--metric", "description": "Whether to include live metrics (only possible for running Camel application)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--node-label", "description": "What text to display in diagram nodes: code, description, or both (default)", "defaultValue": "both", "javaType": "java.lang.String", "type": "string" }, { "names": "--output", "description": "Save diagram to a file (PNG for image themes, text for ascii theme)", "javaType": "java.lang.String", "type": "string" }, { "names": "--theme", "description": "Color theme preset (dark, light, transparent, ascii, unicode) or custom colors (e.g. bg=#1e1e1e:from=#2e7d32:to=#1565c0). Values can be #hex or ANSI color names (e.g. from=seagreen:to=steelblue). Use bg= for transparent. Use ascii\/unicode for plain text output. Can also be set via DIAGRAM_COLORS env var.", "defaultValue": "transparent", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "--width", "description": "Image width in pixels (0 = auto)", "defaultValue": "0", "javaType": "int", "type": "integer" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "route-structure", "fullName": "cmd route-structure", "description": "Dump Camel route structure", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelRouteStructureAction", "options": [ { "names": "--brief", "description": "To show less detailed route structure", "javaType": "boolean", "type": "boolean" }, { "names": "--description", "description": "To show description instead of code", "javaType": "boolean", "type": "boolean" }, { "names": "--filter", "description": "Filter route by filename or route id (multiple names can be separated by comma)", "javaType": "java.lang.String", "type": "string" }, { "names": "--ignore-loading-error", "description": "Whether to ignore route loading and compilation errors (use this with care!)", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--raw", "description": "To output raw without metadata", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort route by name or id", "defaultValue": "name", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "send", "fullName": "cmd send", "description": "Send messages to endpoints", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelSendAction", "options": [ { "names": "--body", "description": "Message body to send (prefix with file: to refer to loading message body from file)", "javaType": "java.lang.String", "type": "string" }, { "names": "--download", "description": "Whether to allow automatic downloading JAR dependencies (over the internet)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--endpoint,--uri", "description": "Endpoint where to send the message (can be uri, pattern, or refer to a route id)", "javaType": "java.lang.String", "type": "string" }, { "names": "--fresh", "description": "Make sure we use fresh (i.e. non-cached) resources", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--header", "description": "Message header (key=value)", "javaType": "java.util.List", "type": "array" }, { "names": "--infra", "description": "Send to infrastructure service (e.g., nats, kafka)", "javaType": "java.lang.String", "type": "string" }, { "names": "--logging-color", "description": "Use colored logging", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--poll", "description": "Poll instead of sending a message. This can be used to receive latest message from a Kafka topic or JMS queue.", "javaType": "boolean", "type": "boolean" }, { "names": "--pretty", "description": "Pretty print response message body (InOut) when using JSon or XML format", "javaType": "boolean", "type": "boolean" }, { "names": "--prop,--property", "description": "Additional properties; override existing (only applicable when NOT using an existing running Camel)", "javaType": "java.lang.String", "type": "string" }, { "names": "--properties", "description": "comma separated list of properties file (only applicable when NOT using an existing running Camel) (ex. \/path\/to\/file.properties,\/path\/to\/other.properties", "javaType": "java.lang.String", "type": "string" }, { "names": "--reply", "description": "Whether to expect a reply message (InOut vs InOut messaging style)", "javaType": "boolean", "type": "boolean" }, { "names": "--reply-file", "description": "Saves reply message to the file with the given name (override if exists)", "javaType": "java.lang.String", "type": "string" }, { "names": "--repo,--repos", "description": "Additional maven repositories for download on-demand (Use commas to separate multiple repositories)", "javaType": "java.lang.String", "type": "string" }, { "names": "--show-body", "description": "Show message body from response message (InOut)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--show-exception", "description": "Show exception and stacktrace for failed messages", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--show-exchange-properties", "description": "Show exchange properties from response message (InOut)", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--show-exchange-variables", "description": "Show exchange variables from response message (InOut)", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--show-headers", "description": "Show message headers from response message (InOut)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--timeout", "description": "Timeout in millis waiting for message to be sent (and reply message if InOut messaging)", "defaultValue": "20000", "javaType": "long", "type": "integer" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "start-group", "fullName": "cmd start-group", "description": "Start Camel route groups", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelRouteGroupStartAction", "options": [ { "names": "--id", "description": "Route ids (multiple ids can be separated by comma)", "defaultValue": "*", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "start-route", "fullName": "cmd start-route", "description": "Start Camel routes", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelRouteStartAction", "options": [ { "names": "--id", "description": "Route ids (multiple ids can be separated by comma)", "defaultValue": "*", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "stop-group", "fullName": "cmd stop-group", "description": "Stop Camel route groups", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelRouteGroupStopAction", "options": [ { "names": "--id", "description": "Route ids (multiple ids can be separated by comma)", "defaultValue": "*", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "stop-route", "fullName": "cmd stop-route", "description": "Stop Camel routes", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelRouteStopAction", "options": [ { "names": "--id", "description": "Route ids (multiple ids can be separated by comma)", "defaultValue": "*", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "stub", "fullName": "cmd stub", "description": "Browse stub endpoints", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelStubAction", "options": [ { "names": "--browse", "description": "Whether to browse messages queued in the stub endpoints", "javaType": "boolean", "type": "boolean" }, { "names": "--compact", "description": "Compact output (no empty line separating browsed messages)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--filter", "description": "Filter endpoints by queue name", "javaType": "java.lang.String", "type": "string" }, { "names": "--find", "description": "Find and highlight matching text (ignore case).", "javaType": "java.lang.String", "type": "string" }, { "names": "--grep", "description": "Filter browsing messages to only output trace matching text (ignore case).", "javaType": "java.lang.String", "type": "string" }, { "names": "--limit", "description": "Filter browsing queues by limiting to the given latest number of messages", "defaultValue": "10", "javaType": "int", "type": "integer" }, { "names": "--logging-color", "description": "Use colored logging", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--mask", "description": "Whether to mask endpoint URIs to avoid printing sensitive information such as password or access keys", "javaType": "boolean", "type": "boolean" }, { "names": "--pretty", "description": "Pretty print message body when using JSon or XML format", "javaType": "boolean", "type": "boolean" }, { "names": "--show-body", "description": "Show message body in traced messages", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--show-headers", "description": "Show message headers in traced messages", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by name, or total", "defaultValue": "name", "javaType": "java.lang.String", "type": "string" }, { "names": "--top", "description": "Whether to browse top (latest) messages queued in the stub endpoints", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "suspend-route", "fullName": "cmd suspend-route", "description": "Suspend Camel routes", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelRouteSuspendAction", "options": [ { "names": "--id", "description": "Route ids (multiple ids can be separated by comma)", "defaultValue": "*", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "thread-dump", "fullName": "cmd thread-dump", "description": "List threads in a running Camel integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelThreadDump", "options": [ { "names": "--depth", "description": "Max depth of stack-trace", "defaultValue": "1", "javaType": "int", "type": "integer" }, { "names": "--filter", "description": "Filter thread names\/ids (use all to include all threads)", "defaultValue": "Camel", "javaType": "java.lang.String", "type": "string" }, { "names": "--sort", "description": "Sort by id, name or state", "defaultValue": "id", "javaType": "java.lang.String", "type": "string" }, { "names": "--state", "description": "To only show threads for a given state", "javaType": "java.lang.String", "type": "string" }, { "names": "--trace", "description": "Include stack-traces", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] } ] }, @@ -11,7 +12,7 @@ { "name": "doc", "fullName": "doc", "description": "Shows documentation for kamelet, component, and other Camel resources", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDoc", "options": [ { "names": "--camel-version", "description": "To use a different Camel version than the default version", "javaType": "java.lang.String", "type": "string" }, { "names": "--download", "description": "Whether to allow automatic downloading JAR dependencies (over the internet)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--example", "description": "Prints a minimal working YAML route snippet for the component", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--filter", "description": "Filter option listed in tables by name, description, or group", "javaType": "java.lang.String", "type": "string" }, { "names": "--fresh", "description": "Make sure we use fresh (i.e. non-cached) resources", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--header", "description": "Whether to display component message headers", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--kamelets-version", "description": "Apache Camel Kamelets version", "defaultValue": "RuntimeType.KAMELETS_VERSION", "javaType": "java.lang.String", "type": "string" }, { "names": "--open-url", "description": "Opens the online documentation form the Camel website in the web browser", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--quarkus-artifact-id", "description": "Deprecated. This value is not used anymore. It is kept only for backwards compatibility and will be removed in Camel 5.x. Camel commands may use either 'quarkus-bom' or 'quarkus-camel-bom' artifactIds depending on the context.", "defaultValue": "quarkus-bom", "javaType": "java.lang.String", "type": "string", "deprecated": true }, { "names": "--quarkus-ext-registry", "description": "The base URI of Quarkus Extension Registry. The default is {@value RuntimeType#QUARKUS_EXTENSION_REGISTRY_BASE_URL} unless camel.jbang.quarkus.platform.url system property is set (the \/client\/platforms suffix is removed if present).", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-group-id", "description": "groupId of Quarkus Platform BOM; honored only if --quarkus-version is set", "defaultValue": "io.quarkus.platform", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-version", "description": "version of Quarkus Platform BOM; the default value is looked up in Quarkus Extension Registry", "javaType": "java.lang.String", "type": "string" }, { "names": "--repo,--repos", "description": "Additional maven repositories for download on-demand (Use commas to separate multiple repositories)", "javaType": "java.lang.String", "type": "string" }, { "names": "--runtime", "description": "Runtime (camel-main, spring-boot, quarkus)", "javaType": "org.apache.camel.dsl.jbang.core.common.RuntimeType", "type": "object" }, { "names": "--url", "description": "Prints the link to the online documentation on the Camel website", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "doctor", "fullName": "doctor", "description": "Checks the environment and reports potential issues", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Doctor", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "eval", "fullName": "eval", "description": "Evaluate Camel expressions and scripts", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression", "fullName": "eval expression", "description": "Evaluates Camel expression", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalExpressionCommand", "options": [ { "names": "--body", "description": "Message body (prefix with file: to refer to loading message body from file)", "javaType": "java.lang.String", "type": "string" }, { "names": "--camel-version", "description": "To run using a different Camel version than the default version.", "javaType": "java.lang.String", "type": "string" }, { "names": "--download", "description": "Whether to allow automatic downloading JAR dependencies (over the internet)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--fresh", "description": "Make sure we use fresh (i.e. non-cached) resources", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--header", "description": "Message header (key=value)", "javaType": "java.util.List", "type": "array" }, { "names": "--isolated", "description": "Whether to run evaluation isolated in local process", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--language", "description": "Language to use", "defaultValue": "simple", "javaType": "java.lang.String", "type": "string" }, { "names": "--predicate", "description": "Whether to force evaluating as predicate", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--repo,--repos", "description": "Additional maven repositories for download on-demand (Use commas to separate multiple repositories)", "javaType": "java.lang.String", "type": "string" }, { "names": "--template", "description": "The template to use for evaluating (prefix with file: to refer to loading template from file)", "javaType": "java.lang.String", "type": "string", "required": true }, { "names": "--timeout", "description": "Timeout in millis waiting for evaluation to be done", "defaultValue": "10000", "javaType": "long", "type": "integer" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] } ] }, - { "name": "explain", "fullName": "explain", "description": "Explain what a Camel route does using AI\/LLM", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names": "--api-key", "description": "API key for authentication. Also reads OPENAI_API_KEY or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama' or 'openai' (OpenAI-compatible)", "defaultValue": "ollama", "javaType": "org.apache.camel.dsl.jbang.core.commands.ApiType", "type": "object" }, { "names": "--catalog-context", "description": "Include Camel Catalog descriptions in the prompt", "javaType": "boolean", "type": "boolean" }, { "names": "--format", "description": "Output format (text, markdown)", "defaultValue": "text", "javaType": "java.lang.String", "type": "string" }, { "names": "--model", "description": "Model to use", "defaultValue": "DEFAULT_MODEL", "javaType": "java.lang.String", "type": "string" }, { "names": "--show-prompt", "description": "Show the prompt sent to the LLM", "javaType": "boolean", "type": "boolean" }, { "names": "--stream", "description": "Stream the response as it's generated (shows progress)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--system-prompt", "description": "Custom system prompt", "javaType": "java.lang.String", "type": "string" }, { "names": "--temperature", "description": "Temperature for response generation (0.0-2.0)", "defaultValue": "0.7", "javaType": "double", "type": "number" }, { "names": "--timeout", "description": "Timeout in seconds for LLM response", "defaultValue": "120", "javaType": "int", "type": "integer" }, { "names": "--url", "description": "LLM API endpoint URL. Auto-detected from 'camel infra' for Ollama if not specified.", "javaType": "java.lang.String", "type": "string" }, { "names": "--verbose,-v", "description": "Include detailed technical information", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, + { "name": "explain", "fullName": "explain", "description": "Explain what a Camel route does using AI\/LLM", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names": "--api-key", "description": "API key for authentication. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama', 'openai' (OpenAI-compatible), or 'anthropic' (Anthropic\/Vertex AI)", "javaType": "LlmClient.ApiType", "type": "object" }, { "names": "--catalog-context", "description": "Include Camel Catalog descriptions in the prompt", "javaType": "boolean", "type": "boolean" }, { "names": "--format", "description": "Output format (text, markdown)", "defaultValue": "text", "javaType": "java.lang.String", "type": "string" }, { "names": "--model", "description": "Model to use", "defaultValue": "DEFAULT_MODEL", "javaType": "java.lang.String", "type": "string" }, { "names": "--show-prompt", "description": "Show the prompt sent to the LLM", "javaType": "boolean", "type": "boolean" }, { "names": "--stream", "description": "Stream the response as it's generated (shows progress)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--system-prompt", "description": "Custom system prompt", "javaType": "java.lang.String", "type": "string" }, { "names": "--temperature", "description": "Temperature for response generation (0.0-2.0)", "defaultValue": "0.7", "javaType": "double", "type": "number" }, { "names": "--timeout", "description": "Timeout in seconds for LLM response", "defaultValue": "120", "javaType": "int", "type": "integer" }, { "names": "--url", "description": "LLM API endpoint URL. Auto-detected from 'camel infra' for Ollama if not specified.", "javaType": "java.lang.String", "type": "string" }, { "names": "--verbose,-v", "description": "Include detailed technical information", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "export", "fullName": "export", "description": "Export to other runtimes (Camel Main, Spring Boot, or Quarkus)", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Export", "options": [ { "names": "--build-property", "description": "Maven build properties, ex. --build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, { "names": "--camel-spring-boot-version", "description": "Camel version to use with Spring Boot", "javaType": "java.lang.String", "type": "string" }, { "names": "--camel-version", "description": "To export using a different Camel version than the default version.", "javaType": "java.lang.String", "type": "string" }, { "names": "--clean-dir", "description": "If exporting to current directory (default) then all existing files are preserved. Enabling this option will force cleaning current directory including all sub dirs (use this with care)", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--dep,--dependency", "description": "Add additional dependencies", "javaType": "java.util.List", "type": "array" }, { "names": "--dir,--directory", "description": "Directory where the project will be exported", "defaultValue": ".", "javaType": "java.lang.String", "type": "string" }, { "names": "--download", "description": "Whether to allow automatic downloading JAR dependencies (over the internet)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--dry-run", "description": "Preview export without writing files", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--exclude", "description": "Exclude files by name or pattern", "javaType": "java.util.List", "type": "array" }, { "names": "--fresh", "description": "Make sure we use fresh (i.e. non-cached) resources", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--gav", "description": "The Maven group:artifact:version", "javaType": "java.lang.String", "type": "string" }, { "names": "--groovy-pre-compiled", "description": "Whether to include pre-compiled Groovy classes in the export (only supported with runtime=camel-main)", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--hawtio", "description": "Whether to include Hawtio web console (only available for exporting to Spring Boot or Quarkus)", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--hawtio-version", "description": "Version of the Hawtio web console", "defaultValue": "HawtioVersion.HAWTIO_VERSION", "javaType": "java.lang.String", "type": "string" }, { "names": "--ignore-loading-error", "description": "Whether to ignore route loading and compilation errors (use this with care!)", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--java-version,--java", "description": "Java version (21, 25)", "defaultValue": "21", "javaType": "java.lang.String", "type": "string" }, { "names": "--kamelets-version", "description": "Apache Camel Kamelets version", "defaultValue": "RuntimeType.KAMELETS_VERSION", "javaType": "java.lang.String", "type": "string" }, { "names": "--lazy-bean", "description": "Whether to use lazy bean initialization (can help with complex classloading issues", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--local-kamelet-dir", "description": "Local directory for loading Kamelets (takes precedence)", "javaType": "java.lang.String", "type": "string" }, { "names": "--logging", "description": "Can be used to turn on logging to console (logs by default to file in \/.camel directory)", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--logging-level", "description": "Logging level (ERROR, WARN, INFO, DEBUG, TRACE)", "defaultValue": "info", "javaType": "java.lang.String", "type": "string" }, { "names": "--main-classname", "description": "The class name of the Camel Main application class", "defaultValue": "CamelApplication", "javaType": "java.lang.String", "type": "string" }, { "names": "--management-port", "description": "To use a dedicated port for HTTP management", "javaType": "int", "type": "integer" }, { "names": "--maven-apache-snapshot-enabled", "description": "Whether downloading JARs from ASF Maven Snapshot repository is enabled", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--maven-central-enabled", "description": "Whether downloading JARs from Maven Central repository is enabled", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--maven-settings", "description": "Optional location of Maven settings.xml file to configure servers, repositories, mirrors and proxies. If set to false, not even the default ~\/.m2\/settings.xml will be used.", "javaType": "java.lang.String", "type": "string" }, { "names": "--maven-settings-security", "description": "Optional location of Maven settings-security.xml file to decrypt settings.xml", "javaType": "java.lang.String", "type": "string" }, { "names": "--maven-wrapper", "description": "Include Maven Wrapper files in exported project", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--name", "description": "The integration name. Use this when the name should not get derived otherwise.", "javaType": "java.lang.String", "type": "string" }, { "names": "--observe", "description": "Enable observability services", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--open-api", "description": "Adds an OpenAPI spec from the given file (json or yaml file)", "javaType": "java.lang.String", "type": "string" }, { "names": "--package-name", "description": "For Java source files should they have the given package name. By default the package name is computed from the Maven GAV. Use false to turn off and not include package name in the Java source files.", "javaType": "java.lang.String", "type": "string" }, { "names": "--package-scan-jars", "description": "Whether to automatic package scan JARs for custom Spring or Quarkus beans making them available for Camel JBang", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--port", "description": "Embeds a local HTTP server on this port", "javaType": "int", "type": "integer" }, { "names": "--profile", "description": "Profile to export (dev, test, prod).", "javaType": "java.lang.String", "type": "string" }, { "names": "--prop,--property", "description": "Camel application properties, ex. --property=prop1=foo", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-artifact-id", "description": "Deprecated. This value is not used anymore. It is kept only for backwards compatibility and will be removed in Camel 5.x. Camel commands may use either 'quarkus-bom' or 'quarkus-camel-bom' artifactIds depending on the context.", "defaultValue": "quarkus-bom", "javaType": "java.lang.String", "type": "string", "deprecated": true }, { "names": "--quarkus-ext-registry", "description": "The base URI of Quarkus Extension Registry. The default is {@value RuntimeType#QUARKUS_EXTENSION_REGISTRY_BASE_URL} unless camel.jbang.quarkus.platform.url system property is set (the \/client\/platforms suffix is removed if present).", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-group-id", "description": "groupId of Quarkus Platform BOM; honored only if --quarkus-version is set", "defaultValue": "io.quarkus.platform", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-package-type", "description": "Quarkus package type (uber-jar or fast-jar)", "defaultValue": "fast-jar", "javaType": "java.lang.String", "type": "string" }, { "names": "--quarkus-version", "description": "version of Quarkus Platform BOM; the default value is looked up in Quarkus Extension Registry", "javaType": "java.lang.String", "type": "string" }, { "names": "--quiet", "description": "Will be quiet, only print when error occurs", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--repo,--repos", "description": "Additional maven repositories for download on-demand (Use commas to separate multiple repositories)", "javaType": "java.lang.String", "type": "string" }, { "names": "--runtime", "description": "Runtime (camel-main, spring-boot, quarkus)", "javaType": "org.apache.camel.dsl.jbang.core.common.RuntimeType", "type": "object" }, { "names": "--skip-plugins", "description": "Skip plugins during export", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--spring-boot-version", "description": "Spring Boot version", "defaultValue": "RuntimeType.SPRING_BOOT_VERSION", "javaType": "java.lang.String", "type": "string" }, { "names": "--verbose", "description": "Verbose output of startup activity (dependency resolution and downloading", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--yes,-y", "description": "Automatically answer yes to confirmation prompts (e.g. when using --clean-dir)", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "get", "fullName": "get", "description": "Get status of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "bean", "fullName": "get bean", "description": "List beans in a running Camel integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelBeanDump", "options": [ { "names": "--dsl", "description": "Include only beans from YAML or XML DSL", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--filter", "description": "Filter beans names (use all to include all beans)", "defaultValue": "all", "javaType": "java.lang.String", "type": "string" }, { "names": "--internal", "description": "Include internal Camel beans", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--nulls", "description": "Include null values", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--properties", "description": "Show bean properties", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by name or type", "defaultValue": "name", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "blocked", "fullName": "get blocked", "description": "Get blocked messages of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListBlocked", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "circuit-breaker", "fullName": "get circuit-breaker", "description": "Get status of Circuit Breaker EIPs", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListCircuitBreaker", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "consumer", "fullName": "get consumer", "description": "Get status of Camel consumers", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListConsumer", "options": [ { "names": "--filter", "description": "Filter consumers by URI", "javaType": "java.lang.String", "type": "string" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--limit", "description": "Filter consumers by limiting to the given number of rows", "javaType": "int", "type": "integer" }, { "names": "--scheduled", "description": "Filter consumer to only show scheduled based consumers", "javaType": "boolean", "type": "boolean" }, { "names": "--short-uri", "description": "List endpoint URI without query parameters (short)", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "--wide-uri", "description": "List endpoint URI in full details", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "context", "fullName": "get context", "description": "Get status of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.CamelContextStatus", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--remote", "description": "Break down counters into remote\/total pairs", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "count", "fullName": "get count", "description": "Get total and failed exchanges", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.CamelCount", "options": [ { "names": "--fail", "description": "Get the failed exchanges from a running integration", "javaType": "boolean", "type": "boolean" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--total", "description": "Get the total exchanges from a running integration", "javaType": "boolean", "type": "boolean" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "endpoint", "fullName": "get endpoint", "description": "Get usage of Camel endpoints", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListEndpoint", "options": [ { "names": "--filter", "description": "Filter endpoints by URI", "javaType": "java.lang.String", "type": "string" }, { "names": "--filter-direction", "description": "Filter by direction (in or out)", "javaType": "java.lang.String", "type": "string" }, { "names": "--filter-total", "description": "Filter endpoints that must be higher than the given usage", "javaType": "long", "type": "integer" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--limit", "description": "Filter endpoints by limiting to the given number of rows", "javaType": "int", "type": "integer" }, { "names": "--short-uri", "description": "List endpoint URI without query parameters (short)", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name, age or total", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "--wide-uri", "description": "List endpoint URI in full details", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "event", "fullName": "get event", "description": "Get latest events of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListEvent", "options": [ { "names": "--filter", "description": "Filter event by event type: context, route, or exchange", "javaType": "java.lang.String", "type": "string" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "groovy", "fullName": "get groovy", "description": "Groovy Sources used of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListGroovy", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid or name", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "group", "fullName": "get group", "description": "Get status of Camel route groups", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.CamelRouteGroupStatus", "options": [ { "names": "--filter", "description": "Filter groups by name", "javaType": "java.lang.String", "type": "string" }, { "names": "--filter-mean", "description": "Filter groups that must be slower than the given time (ms)", "javaType": "long", "type": "integer" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--limit", "description": "Filter groups by limiting to the given number of rows", "javaType": "int", "type": "integer" }, { "names": "--running", "description": "Only include running groups", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name, age or group", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "health", "fullName": "get health", "description": "Get health check status of running Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListHealth", "options": [ { "names": "--depth", "description": "Max depth of stack-trace", "defaultValue": "1", "javaType": "int", "type": "integer" }, { "names": "--down", "description": "Show only checks which are DOWN", "javaType": "boolean", "type": "boolean" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--level", "description": "Level of details: full, or default", "defaultValue": "default", "javaType": "java.lang.String", "type": "string" }, { "names": "--live", "description": "Show only liveness checks", "javaType": "boolean", "type": "boolean" }, { "names": "--ready", "description": "Show only readiness checks", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--trace", "description": "Include stack-traces in error messages", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "history", "fullName": "get history", "description": "History of latest completed exchange", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelHistoryAction", "options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": "--depth", "description": "Depth of tracing. 0=Created Completed. 1=All events on 1st route, 2=All events on 1st 2nd depth, and so on. 9 = all events on every depth.", "defaultValue": "9", "javaType": "int", "type": "integer" }, { "names": "--it", "description": "Interactive mode for enhanced history information", "javaType": "boolean", "type": "boolean" }, { "names": "--limit-split", "description": "Limit Split to a maximum number of entries to be displayed", "javaType": "int", "type": "integer" }, { "names": "--logging-color", "description": "Use colored logging", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--mask", "description": "Whether to mask endpoint URIs to avoid printing sensitive information such as password or access keys", "javaType": "boolean", "type": "boolean" }, { "names": "--pretty", "description": "Pretty print message body when using JSon or XML format", "javaType": "boolean", "type": "boolean" }, { "names": "--show-body", "description": "Show message body in debug messages", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--show-exception", "description": "Show exception and stacktrace for failed messages", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--show-exchange-properties", "description": "Show exchange properties in debug messages", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--show-exchange-variables", "description": "Show exchange variables in debug messages", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--show-headers", "description": "Show message headers in debug messages", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--source", "description": "Prefer to display source filename\/code instead of IDs", "javaType": "boolean", "type": "boolean" }, { "names": "--timestamp", "description": "Print timestamp.", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "inflight", "fullName": "get inflight", "description": "Get inflight messages of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListInflight", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "internal-task", "fullName": "get internal-task", "description": "List internal tasks of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListInternalTask", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "kafka", "fullName": "get kafka", "description": "List Kafka consumers of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListKafka", "options": [ { "names": "--committed", "description": "Show committed offset (slower due to sync call to Kafka brokers)", "javaType": "boolean", "type": "boolean" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--short-uri", "description": "List endpoint URI without query parameters (short)", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "--wide-uri", "description": "List endpoint URI in full details", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "metric", "fullName": "get metric", "description": "Get metrics (micrometer) of running Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListMetric", "options": [ { "names": "--all", "description": "Whether to show all metrics (also unused with counter being 0)", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--custom", "description": "Only show custom metrics", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--filter", "description": "Filter metric by type, name or tags", "javaType": "java.lang.String", "type": "string" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--tags", "description": "Show metric tags", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "platform-http", "fullName": "get platform-http", "description": "Get embedded HTTP services of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListPlatformHttp", "options": [ { "names": "--all", "description": "Include management endpoints", "javaType": "boolean", "type": "boolean" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "processor", "fullName": "get processor", "description": "Get status of Camel processors", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.CamelProcessorStatus", "options": [ { "names": "--description", "description": "Include description in the ID column (if available)", "javaType": "boolean", "type": "boolean" }, { "names": "--filter", "description": "Filter processors by id", "javaType": "java.lang.String", "type": "string" }, { "names": "--filter-mean", "description": "Filter processors that must be slower than the given time (ms)", "javaType": "long", "type": "integer" }, { "names": "--group", "description": "Filter processors by group", "javaType": "java.lang.String", "type": "string" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--limit", "description": "Filter routes by limiting to the given number of rows", "javaType": "int", "type": "integer" }, { "names": "--note", "description": "Include note in the ID column (if available)", "javaType": "boolean", "type": "boolean" }, { "names": "--remote", "description": "Break down counters into remote\/total pairs", "javaType": "boolean", "type": "boolean" }, { "names": "--running", "description": "Only include running processors", "javaType": "boolean", "type": "boolean" }, { "names": "--show-group", "description": "Include group column", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid or name", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--source", "description": "Prefer to display source filename\/code instead of IDs", "javaType": "boolean", "type": "boolean" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "producer", "fullName": "get producer", "description": "Get status of Camel producers", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListProducer", "options": [ { "names": "--filter", "description": "Filter producers by URI", "javaType": "java.lang.String", "type": "string" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--limit", "description": "Filter producers by limiting to the given number of rows", "javaType": "int", "type": "integer" }, { "names": "--short-uri", "description": "List endpoint URI without query parameters (short)", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "--wide-uri", "description": "List endpoint URI in full details", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "properties", "fullName": "get properties", "description": "List configuration properties", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListProperties", "options": [ { "names": "--internal", "description": "Whether to include internal configuration", "javaType": "boolean", "type": "boolean" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--mask", "description": "Whether to mask configuration values to avoid printing sensitive information such as password or access keys", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or key", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--startup", "description": "List only startup configuration", "javaType": "boolean", "type": "boolean" }, { "names": "--verbose", "description": "Whether to include more details", "javaType": "boolean", "type": "boolean" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "rest", "fullName": "get rest", "description": "Get REST services of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListRest", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--verbose", "description": "Show more details", "javaType": "boolean", "type": "boolean" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "route", "fullName": "get route", "description": "Get status of Camel routes", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.CamelRouteStatus", "options": [ { "names": "--description", "description": "Include description in the ID column (if available)", "javaType": "boolean", "type": "boolean" }, { "names": "--error", "description": "Shows detailed information for routes that has error status", "javaType": "boolean", "type": "boolean" }, { "names": "--filter", "description": "Filter routes by id, or url", "javaType": "java.lang.String", "type": "string" }, { "names": "--filter-mean", "description": "Filter routes that must be slower than the given time (ms)", "javaType": "long", "type": "integer" }, { "names": "--group", "description": "Filter routes by group", "javaType": "java.lang.String", "type": "string" }, { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--limit", "description": "Filter routes by limiting to the given number of rows", "javaType": "int", "type": "integer" }, { "names": "--note", "description": "Include note in the ID column (if available)", "javaType": "boolean", "type": "boolean" }, { "names": "--running", "description": "Only include running routes", "javaType": "boolean", "type": "boolean" }, { "names": "--short-uri", "description": "List endpoint URI without query parameters (short)", "javaType": "boolean", "type": "boolean" }, { "names": "--show-group", "description": "Include group column", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--source", "description": "Prefer to display source filename\/code instead of IDs", "javaType": "boolean", "type": "boolean" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "--wide-uri", "description": "List endpoint URI in full details", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "route-controller", "fullName": "get route-controller", "description": "List status of route controller", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.RouteControllerAction", "options": [ { "names": "--depth", "description": "Max depth of stack-trace", "defaultValue": "1", "javaType": "int", "type": "integer" }, { "names": "--header", "description": "Include controller configuration details", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by id, or state", "defaultValue": "id", "javaType": "java.lang.String", "type": "string" }, { "names": "--trace", "description": "Include stack-traces in error messages", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "route-dump", "fullName": "get route-dump", "description": "Dump Camel route in XML or YAML format", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelRouteDumpAction", "options": [ { "names": "--filter", "description": "Filter route by filename or route id (multiple names can be separated by comma)", "javaType": "java.lang.String", "type": "string" }, { "names": "--format", "description": "Output format (xml, or yaml)", "defaultValue": "yaml", "javaType": "java.lang.String", "type": "string" }, { "names": "--raw", "description": "To output raw without metadata", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort route by name or id", "defaultValue": "name", "javaType": "java.lang.String", "type": "string" }, { "names": "--uri-as-parameters", "description": "Whether to expand URIs into separated key\/value parameters (only in use for YAML format)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "service", "fullName": "get service", "description": "Get services of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListService", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--metadata", "description": "Show service metadata (only available for some services)", "javaType": "boolean", "type": "boolean" }, { "names": "--short-uri", "description": "List endpoint URI without query parameters (short)", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or age", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "--wide-uri", "description": "List endpoint URI in full details", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "source", "fullName": "get source", "description": "Display Camel route source code", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelSourceAction", "options": [ { "names": "--filter", "description": "Filter source by filename (multiple names can be separated by comma)", "javaType": "java.lang.String", "type": "string" }, { "names": "--sort", "description": "Sort source by name", "defaultValue": "name", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "startup-recorder", "fullName": "get startup-recorder", "description": "Display startup recording", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelStartupRecorderAction", "options": [ { "names": "--sort", "description": "Sort by duration, or type", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "transformer", "fullName": "get transformer", "description": "Get list of data type transformers", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListTransformer", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name, age or total", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "variable", "fullName": "get variable", "description": "List variables of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListVariable", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name or key", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "vault", "fullName": "get vault", "description": "List secrets from security vaults", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListVault", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--sort", "description": "Sort by pid, name", "defaultValue": "pid", "javaType": "java.lang.String", "type": "string" }, { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] } ] }, { "name": "harden", "fullName": "harden", "description": "Suggest security hardening for Camel routes using AI\/LLM", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Harden", "options": [ { "names": "--api-key", "description": "API key for authentication. Also reads OPENAI_API_KEY or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama' or 'openai' (OpenAI-compatible)", "defaultValue": "ollama", "javaType": "org.apache.camel.dsl.jbang.core.commands.ApiType", "type": "object" }, { "names": "--catalog-context", "description": "Include Camel Catalog descriptions in the prompt", "javaType": "boolean", "type": "boolean" }, { "names": "--format", "description": "Output format (text, markdown)", "defaultValue": "text", "javaType": "java.lang.String", "type": "string" }, { "names": "--model", "description": "Model to use", "defaultValue": "DEFAULT_MODEL", "javaType": "java.lang.String", "type": "string" }, { "names": "--show-prompt", "description": "Show the prompt sent to the LLM", "javaType": "boolean", "type": "boolean" }, { "names": "--stream", "description": "Stream the response as it's generated (shows progress)", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--system-prompt", "description": "Custom system prompt", "javaType": "java.lang.String", "type": "string" }, { "names": "--temperature", "description": "Temperature for response generation (0.0-2.0)", "defaultValue": "0.7", "javaType": "double", "type": "number" }, { "names": "--timeout", "description": "Timeout in seconds for LLM response", "defaultValue": "120", "javaType": "int", "type": "integer" }, { "names": "--url", "description": "LLM API endpoint URL. Auto-detected from 'camel infra' for Ollama if not specified.", "javaType": "java.lang.String", "type": "string" }, { "names": "--verbose,-v", "description": "Include detailed security recommendations with code examples", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java new file mode 100644 index 0000000000000..0a7010c6e3a31 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java @@ -0,0 +1,838 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; +import org.apache.camel.dsl.jbang.core.common.ExampleHelper; +import org.apache.camel.dsl.jbang.core.common.RuntimeHelper; +import org.apache.camel.tooling.model.ComponentModel; +import org.apache.camel.util.IOHelper; +import org.apache.camel.util.json.JsonObject; +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.UserInterruptException; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Ask a question about a running Camel application using AI with tool calling. The LLM can inspect the live runtime + * (routes, health, traces, etc.) to provide informed answers. + */ +@Command(name = "ask", + description = "Ask a question about a running Camel application using AI", + sortOptions = false, showDefaultValues = true, + footer = { + "%nExamples:", + " camel ask \"what routes are running?\"", + " camel ask \"why is my route failing?\" --name=myApp", + " camel ask \"show me the route structure\" --api-type=anthropic", + " camel ask \"are there any blocked exchanges?\" --model=gpt-4", + " camel ask (interactive chat)" }) +public class Ask extends CamelCommand { + + private static final String DEFAULT_MODEL = "llama3.2"; + private static final String NO_PROCESS + = "No running Camel process connected. Start one with: camel run "; + + @Parameters(description = "Question to ask (omit for interactive chat mode)", arity = "0..*") + List question; + + @Option(names = { "--url" }, + description = "LLM API endpoint URL. Auto-detected if not specified.") + String url; + + @Option(names = { "--api-type" }, + description = "API type: 'ollama', 'openai', or 'anthropic'") + LlmClient.ApiType apiType; + + @Option(names = { "--api-key" }, + description = "API key. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY env vars") + String apiKey; + + @Option(names = { "--model" }, + description = "Model to use", + defaultValue = DEFAULT_MODEL) + String model = DEFAULT_MODEL; + + @Option(names = { "--timeout" }, + description = "Timeout in seconds for LLM response", + defaultValue = "120") + int timeout = 120; + + @Option(names = { "--name" }, + description = "Name or PID of the Camel process. Auto-detected when exactly one process is running") + String nameOrPid; + + @Option(names = { "--max-iterations" }, + description = "Maximum number of tool-calling rounds", + defaultValue = "10") + int maxIterations = 10; + + @Option(names = { "--show-tools" }, + description = "Show tool calls and results as they happen") + boolean showTools; + + @Option(names = { "--verbose" }, + description = "Print debug information: HTTP requests, responses, and parsed results") + boolean verbose; + + private long targetPid; + private CamelCatalog catalog; + + public Ask(CamelJBangMain main) { + super(main); + } + + @Override + public Integer doCall() throws Exception { + LlmClient client = LlmClient.create() + .withUrl(url) + .withApiType(apiType) + .withApiKey(apiKey) + .withModel(model) + .withTimeout(timeout) + .withTemperature(0.3) + .withStream(true) + .withMaxTokens(4096) + .withVerbose(verbose) + .withPrinter(printer()); + + if (!client.detectEndpoint()) { + printer().printErr("LLM service is not reachable."); + printer().printErr("Options: --url=, --api-type=anthropic, or start Ollama with: camel infra run ollama"); + return 1; + } + + RuntimeHelper.ProcessInfo process = findProcess(nameOrPid); + if (process != null) { + targetPid = process.pid(); + } else if (nameOrPid != null && !nameOrPid.isBlank()) { + return 1; + } else { + targetPid = -1; + } + + String systemPrompt = buildSystemPrompt(process); + List tools = buildToolDefinitions(); + + if (question == null || question.isEmpty()) { + return runInteractiveChat(client, process, systemPrompt, tools); + } + + String userQuestion = String.join(" ", question); + printer().println("Using " + client.model + " (" + client.apiType + ") to answer your question..."); + if (process != null) { + printer().println("Target: " + process.name() + " (PID " + process.pid() + ")"); + } + printer().println(); + + List messages = new ArrayList<>(); + return runAgentLoop(client, systemPrompt, tools, messages, userQuestion); + } + + private int runInteractiveChat( + LlmClient client, RuntimeHelper.ProcessInfo process, + String systemPrompt, List tools) + throws Exception { + Terminal terminal = EnvironmentHelper.getActiveTerminal(); + if (terminal == null) { + terminal = TerminalBuilder.builder().system(true).build(); + } + LineReader reader = LineReaderBuilder.builder().terminal(terminal).build(); + + printer().println("Camel AI Assistant (" + client.model + ", " + client.apiType + ")"); + if (process != null) { + printer().println("Target: " + process.name() + " (PID " + process.pid() + ")"); + } + printer().println("Type your question, or 'exit' to quit."); + printer().println(); + + List messages = new ArrayList<>(); + + while (true) { + String line; + try { + line = reader.readLine("ask> "); + } catch (UserInterruptException | EndOfFileException e) { + break; + } + if (line == null || line.isBlank() || "exit".equalsIgnoreCase(line.strip())) { + break; + } + + int result = runAgentLoop(client, systemPrompt, tools, messages, line.strip()); + if (result != 0) { + printer().printErr("(error processing question, continuing...)"); + } + printer().println(); + } + return 0; + } + + private int runAgentLoop( + LlmClient client, String systemPrompt, + List tools, List messages, + String userQuestion) { + messages.add(LlmClient.Message.user(userQuestion)); + + for (int i = 0; i < maxIterations; i++) { + LlmClient.ChatResponse response = client.chatWithTools(systemPrompt, messages, tools); + if (response == null) { + printer().printErr("Failed to get response from LLM"); + return 1; + } + + if (response.toolCalls() != null && !response.toolCalls().isEmpty()) { + messages.add(LlmClient.Message.assistantWithToolCalls(response.text(), response.toolCalls())); + + List results = new ArrayList<>(); + for (LlmClient.ToolCall toolCall : response.toolCalls()) { + if (showTools) { + printer().println("[tool] " + toolCall.name() + "(" + toolCall.arguments().toJson() + ")"); + } + String result = executeTool(toolCall.name(), toolCall.arguments()); + if (showTools) { + printer().println("[result] " + truncate(result, 200)); + } + results.add(new LlmClient.ToolResult(toolCall.id(), result)); + } + messages.add(LlmClient.Message.toolResults(results)); + } else { + if (!response.streamed() && response.text() != null) { + printer().println(response.text()); + } + messages.add(LlmClient.Message.assistantWithToolCalls(response.text(), List.of())); + return 0; + } + } + + printer().printErr("Reached maximum iterations (" + maxIterations + ") without a final answer."); + return 1; + } + + // ---- Process discovery (delegates to RuntimeHelper) ---- + + private RuntimeHelper.ProcessInfo findProcess(String nameOrPid) { + RuntimeHelper.ProcessInfo found = RuntimeHelper.findProcess(nameOrPid); + if (found != null) { + return found; + } + + if (nameOrPid != null && !nameOrPid.isBlank()) { + List processes = RuntimeHelper.discoverProcesses(); + if (processes.isEmpty()) { + printer().printErr("No running Camel processes found."); + printer().printErr("Start a Camel application first: camel run myRoute.yaml"); + } else if (processes.size() > 1) { + printer().printErr("No unique Camel process found matching: " + nameOrPid); + processes.forEach(p -> printer().printErr(" " + p.name() + " (PID " + p.pid() + ")")); + printer().printErr("Specify a more specific name or PID with --name"); + } else { + printer().printErr("No Camel process found matching: " + nameOrPid); + } + } + return null; + } + + // ---- System prompt ---- + + private String buildSystemPrompt(RuntimeHelper.ProcessInfo process) { + StringBuilder sb = new StringBuilder(); + sb.append("You are an Apache Camel assistant. "); + sb.append("You help users build, understand, and troubleshoot Camel applications.\n\n"); + + if (process != null) { + sb.append("You are connected to a running Camel application: "); + sb.append(process.name()).append(" (PID ").append(process.pid()).append("). "); + sb.append("Use the runtime inspection tools to gather information about it.\n\n"); + } + + sb.append("You can search the Camel catalog (components, EIPs), browse examples, "); + sb.append("and read/write files to create route definitions.\n\n"); + sb.append("Guidelines:\n"); + sb.append("- When creating routes, use YAML DSL format (Camel's recommended format for JBang)\n"); + sb.append("- Look at existing files first with list_files/read_file before creating new ones\n"); + sb.append("- Use catalog tools to look up component syntax before writing routes\n"); + sb.append("- Use examples as reference when building new routes\n"); + sb.append("- Be concise and actionable in your answers\n"); + sb.append("- Format output as plain text for terminal display, do not use markdown\n"); + if (process != null) { + sb.append("- Start by gathering relevant information using the available runtime tools\n"); + sb.append("- If something looks wrong, explain what it means and suggest fixes\n"); + sb.append("- To stop routes or the application, always use the provided tools "); + sb.append("(stop_route, stop_application) for graceful shutdown. Never suggest kill or kill -9.\n"); + } + return sb.toString(); + } + + // ---- Tool definitions ---- + + private List buildToolDefinitions() { + List tools = new ArrayList<>(); + + // Status-file tools (no parameters needed) + tools.add(new LlmClient.ToolDef( + "get_context", + "Get Camel context info: name, version, state, uptime, route count, exchange statistics.", + emptyParams())); + tools.add(new LlmClient.ToolDef( + "get_routes", + "List all routes with their state, uptime, messages processed, last error, and throughput.", + emptyParams())); + tools.add(new LlmClient.ToolDef( + "get_health", + "Get health check status for the Camel application.", + emptyParams())); + tools.add(new LlmClient.ToolDef( + "get_endpoints", + "List all endpoints registered in the Camel context with URIs and usage stats.", + emptyParams())); + tools.add(new LlmClient.ToolDef( + "get_inflight", + "Show currently in-flight exchanges (messages being processed).", + emptyParams())); + tools.add(new LlmClient.ToolDef( + "get_blocked", + "Show blocked exchanges that are stuck or waiting.", + emptyParams())); + tools.add(new LlmClient.ToolDef( + "get_consumers", + "Show consumer statistics (polling and event-driven consumers).", + emptyParams())); + tools.add(new LlmClient.ToolDef( + "get_properties", + "Show configuration properties of the running Camel application.", + emptyParams())); + + // IPC action tools (with parameters) + tools.add(new LlmClient.ToolDef( + "get_route_source", + "Get the source code of routes. Use filter to limit by filename (supports wildcards).", + objectParams(Map.of( + "filter", stringProp("Filter source files by name (supports wildcards). Use * for all."))))); + tools.add(new LlmClient.ToolDef( + "get_route_dump", + "Dump route definitions in XML or YAML format.", + objectParams(Map.of( + "routeId", stringProp("Route ID to dump (use * for all routes)"), + "format", stringProp("Output format: xml or yaml (default: yaml)"))))); + tools.add(new LlmClient.ToolDef( + "get_route_structure", + "Show the route structure as a tree of processors.", + objectParams(Map.of( + "routeId", stringProp("Route ID to inspect (use * for all routes)"))))); + tools.add(new LlmClient.ToolDef( + "get_top_processors", + "Show top processor statistics: which processors are slowest and most active.", + emptyParams())); + tools.add(new LlmClient.ToolDef( + "trace_control", + "Enable, disable, or dump message tracing.", + objectParams(Map.of( + "action", stringProp("Action: enable, disable, or dump"))))); + + // Route lifecycle tools + tools.add(new LlmClient.ToolDef( + "stop_route", + "Gracefully stop a route. The route will finish processing in-flight exchanges before stopping.", + objectParams(Map.of( + "routeId", stringProp("The ID of the route to stop"))))); + tools.add(new LlmClient.ToolDef( + "start_route", + "Start a stopped route.", + objectParams(Map.of( + "routeId", stringProp("The ID of the route to start"))))); + tools.add(new LlmClient.ToolDef( + "suspend_route", + "Suspend a route (pauses the consumer but keeps the route loaded).", + objectParams(Map.of( + "routeId", stringProp("The ID of the route to suspend"))))); + tools.add(new LlmClient.ToolDef( + "resume_route", + "Resume a suspended route.", + objectParams(Map.of( + "routeId", stringProp("The ID of the route to resume"))))); + + // Application lifecycle + tools.add(new LlmClient.ToolDef( + "stop_application", + "Gracefully stop the Camel application. The application will finish processing in-flight exchanges and shut down cleanly. Use this instead of kill.", + emptyParams())); + + // Catalog tools + tools.add(new LlmClient.ToolDef( + "catalog_components", + "Search the Camel component catalog by name or label. Returns component name, title, description, and labels.", + objectParams(Map.of( + "filter", stringProp("Filter by name, title, or description (case-insensitive substring)"), + "label", stringProp("Filter by category label (e.g., cloud, messaging, database, file)"))))); + tools.add(new LlmClient.ToolDef( + "catalog_component_doc", + "Get detailed documentation for a Camel component: URI syntax and endpoint options.", + objectParams(Map.of( + "component", stringProp("Component name (e.g., kafka, http, file, timer)"))))); + tools.add(new LlmClient.ToolDef( + "catalog_eips", + "Search EIPs (Enterprise Integration Patterns) like split, aggregate, filter, choice, multicast.", + objectParams(Map.of( + "filter", stringProp("Filter by name, title, or description (case-insensitive substring)"))))); + + // Example tools + tools.add(new LlmClient.ToolDef( + "list_examples", + "List available Camel JBang examples. Returns name, title, description, difficulty level, and tags.", + objectParams(Map.of( + "filter", stringProp("Filter by name, description, or tag (case-insensitive)"), + "level", stringProp("Filter by difficulty: beginner, intermediate, or advanced"))))); + tools.add(new LlmClient.ToolDef( + "get_example_file", + "Get the content of a file from a bundled Camel JBang example. Use list_examples first to find available examples.", + objectParams(Map.of( + "example", stringProp("Example name (e.g., timer-log, rest-api, circuit-breaker)"), + "file", stringProp("File name within the example (e.g., route.camel.yaml)"))))); + + // File tools + tools.add(new LlmClient.ToolDef( + "list_files", + "List files in a directory (up to 2 levels deep). Defaults to current working directory.", + objectParams(Map.of( + "path", stringProp("Directory path relative to CWD (default: current directory)"))))); + tools.add(new LlmClient.ToolDef( + "read_file", + "Read the content of a file. Useful for inspecting route definitions, configuration, and properties files.", + objectParams(Map.of( + "file", stringProp("File path relative to CWD"))))); + tools.add(new LlmClient.ToolDef( + "write_file", + "Write content to a file. Creates parent directories if needed. Use for creating or editing route definitions and configuration files.", + objectParams(Map.of( + "file", stringProp("File path relative to CWD"), + "content", stringProp("The content to write to the file"))))); + + return tools; + } + + // ---- Tool execution ---- + + private String executeTool(String name, JsonObject args) { + try { + return switch (name) { + // Runtime tools (require a running process) + case "get_context" -> + targetPid < 0 ? NO_PROCESS : RuntimeHelper.readStatusSection(targetPid, "context"); + case "get_routes" -> + targetPid < 0 ? NO_PROCESS : RuntimeHelper.readStatusSection(targetPid, "routes"); + case "get_health" -> + targetPid < 0 ? NO_PROCESS : RuntimeHelper.readStatusSection(targetPid, "healthChecks"); + case "get_endpoints" -> + targetPid < 0 ? NO_PROCESS : RuntimeHelper.readStatusSection(targetPid, "endpoints"); + case "get_inflight" -> + targetPid < 0 ? NO_PROCESS : RuntimeHelper.readStatusSection(targetPid, "inflight"); + case "get_blocked" -> + targetPid < 0 ? NO_PROCESS : RuntimeHelper.readStatusSection(targetPid, "blocked"); + case "get_consumers" -> + targetPid < 0 ? NO_PROCESS : RuntimeHelper.readStatusSection(targetPid, "consumers"); + case "get_properties" -> + targetPid < 0 ? NO_PROCESS : RuntimeHelper.readStatusSection(targetPid, "properties"); + case "get_route_source" -> targetPid < 0 ? NO_PROCESS : executeRouteSource(args); + case "get_route_dump" -> targetPid < 0 ? NO_PROCESS : executeRouteDump(args); + case "get_route_structure" -> targetPid < 0 ? NO_PROCESS : executeRouteStructure(args); + case "get_top_processors" -> + targetPid < 0 ? NO_PROCESS : RuntimeHelper.executeAction(targetPid, "top-processors", null); + case "trace_control" -> targetPid < 0 ? NO_PROCESS : executeTraceControl(args); + case "stop_route" -> targetPid < 0 ? NO_PROCESS : executeRouteCommand(args, "stop"); + case "start_route" -> targetPid < 0 ? NO_PROCESS : executeRouteCommand(args, "start"); + case "suspend_route" -> targetPid < 0 ? NO_PROCESS : executeRouteCommand(args, "suspend"); + case "resume_route" -> targetPid < 0 ? NO_PROCESS : executeRouteCommand(args, "resume"); + case "stop_application" -> targetPid < 0 ? NO_PROCESS : RuntimeHelper.stopApplication(targetPid); + // Catalog tools + case "catalog_components" -> executeCatalogComponents(args); + case "catalog_component_doc" -> executeCatalogComponentDoc(args); + case "catalog_eips" -> executeCatalogEips(args); + // Example tools + case "list_examples" -> executeListExamples(args); + case "get_example_file" -> executeGetExampleFile(args); + // File tools + case "list_files" -> executeListFiles(args); + case "read_file" -> executeReadFile(args); + case "write_file" -> executeWriteFile(args); + default -> "Unknown tool: " + name; + }; + } catch (Exception e) { + return "Error executing " + name + ": " + e.getMessage(); + } + } + + private String executeRouteSource(JsonObject args) { + String filter = args.getString("filter"); + return RuntimeHelper.executeAction(targetPid, "source", + root -> root.put("filter", filter != null ? filter : "*")); + } + + private String executeRouteDump(JsonObject args) { + String routeId = args.getString("routeId"); + String format = args.getString("format"); + return RuntimeHelper.executeAction(targetPid, "route-dump", root -> { + root.put("id", routeId != null ? routeId : "*"); + root.put("format", format != null ? format : "yaml"); + }); + } + + private String executeRouteStructure(JsonObject args) { + String routeId = args.getString("routeId"); + return RuntimeHelper.executeAction(targetPid, "route-structure", + root -> root.put("id", routeId != null ? routeId : "*")); + } + + private String executeTraceControl(JsonObject args) { + String action = args.getString("action"); + if (action == null) { + return "Error: action is required (enable, disable, dump)"; + } + return RuntimeHelper.executeAction(targetPid, "trace", root -> { + switch (action.toLowerCase()) { + case "enable" -> root.put("enabled", "true"); + case "disable" -> root.put("enabled", "false"); + case "dump" -> root.put("dump", "true"); + default -> root.put("enabled", action); + } + }); + } + + private String executeRouteCommand(JsonObject args, String command) { + String routeId = args.getString("routeId"); + if (routeId == null || routeId.isBlank()) { + return "Error: routeId is required"; + } + return RuntimeHelper.executeAction(targetPid, "route", root -> { + root.put("id", routeId); + root.put("command", command); + }); + } + + // ---- Catalog tools ---- + + private String executeCatalogComponents(JsonObject args) { + String filter = args.getString("filter"); + String label = args.getString("label"); + CamelCatalog catalog = getCatalog(); + + List results = catalog.findComponentNames().stream() + .map(catalog::componentModel) + .filter(m -> m != null) + .filter(m -> matchesFilter(m.getScheme(), m.getTitle(), m.getDescription(), filter)) + .filter(m -> label == null || label.isBlank() + || (m.getLabel() != null && m.getLabel().toLowerCase().contains(label.toLowerCase()))) + .limit(20) + .map(m -> { + JsonObject jo = new JsonObject(); + jo.put("name", m.getScheme()); + jo.put("title", m.getTitle()); + jo.put("description", m.getDescription()); + jo.put("label", m.getLabel()); + return jo; + }) + .collect(Collectors.toList()); + + JsonObject response = new JsonObject(); + response.put("count", results.size()); + response.put("components", results); + return response.toJson(); + } + + private String executeCatalogComponentDoc(JsonObject args) { + String component = args.getString("component"); + if (component == null || component.isBlank()) { + return "Error: component name is required"; + } + CamelCatalog catalog = getCatalog(); + ComponentModel model = catalog.componentModel(component); + if (model == null) { + return "Component not found: " + component; + } + + JsonObject response = new JsonObject(); + response.put("name", model.getScheme()); + response.put("title", model.getTitle()); + response.put("description", model.getDescription()); + response.put("syntax", model.getSyntax()); + response.put("consumerOnly", model.isConsumerOnly()); + response.put("producerOnly", model.isProducerOnly()); + + List options = new ArrayList<>(); + if (model.getEndpointOptions() != null) { + model.getEndpointOptions().stream() + .filter(opt -> !opt.isDeprecated()) + .forEach(opt -> { + JsonObject jo = new JsonObject(); + jo.put("name", opt.getName()); + jo.put("description", opt.getDescription()); + jo.put("type", opt.getType()); + jo.put("required", opt.isRequired()); + if (opt.getDefaultValue() != null) { + jo.put("defaultValue", opt.getDefaultValue().toString()); + } + options.add(jo); + }); + } + response.put("options", options); + return response.toJson(); + } + + private String executeCatalogEips(JsonObject args) { + String filter = args.getString("filter"); + CamelCatalog catalog = getCatalog(); + + List results = catalog.findModelNames().stream() + .map(catalog::eipModel) + .filter(m -> m != null) + .filter(m -> matchesFilter(m.getName(), m.getTitle(), m.getDescription(), filter)) + .limit(20) + .map(m -> { + JsonObject jo = new JsonObject(); + jo.put("name", m.getName()); + jo.put("title", m.getTitle()); + jo.put("description", m.getDescription()); + jo.put("label", m.getLabel()); + return jo; + }) + .collect(Collectors.toList()); + + JsonObject response = new JsonObject(); + response.put("count", results.size()); + response.put("eips", results); + return response.toJson(); + } + + private static boolean matchesFilter(String name, String title, String description, String filter) { + if (filter == null || filter.isBlank()) { + return true; + } + String lf = filter.toLowerCase(); + return (name != null && name.toLowerCase().contains(lf)) + || (title != null && title.toLowerCase().contains(lf)) + || (description != null && description.toLowerCase().contains(lf)); + } + + // ---- Example tools ---- + + @SuppressWarnings("unchecked") + private String executeListExamples(JsonObject args) { + String filter = args.getString("filter"); + String level = args.getString("level"); + + List catalog = ExampleHelper.loadCatalog(); + List filtered = ExampleHelper.filterExamples(catalog, filter); + + List results = new ArrayList<>(); + for (JsonObject entry : filtered) { + if (level != null && !level.isBlank()) { + String entryLevel = entry.getString("level"); + if (entryLevel == null || !entryLevel.equalsIgnoreCase(level)) { + continue; + } + } + JsonObject jo = new JsonObject(); + jo.put("name", entry.getString("name")); + jo.put("title", entry.getString("title")); + jo.put("description", entry.getString("description")); + jo.put("level", entry.getString("level")); + jo.put("tags", entry.get("tags")); + jo.put("bundled", ExampleHelper.isBundled(entry)); + jo.put("files", ExampleHelper.getFiles(entry)); + results.add(jo); + + if (results.size() >= 20) { + break; + } + } + + JsonObject response = new JsonObject(); + response.put("count", results.size()); + response.put("examples", results); + return response.toJson(); + } + + private String executeGetExampleFile(JsonObject args) { + String example = args.getString("example"); + String file = args.getString("file"); + if (example == null || example.isBlank()) { + return "Error: example name is required"; + } + if (file == null || file.isBlank()) { + return "Error: file name is required"; + } + + List catalog = ExampleHelper.loadCatalog(); + JsonObject entry = ExampleHelper.findExample(catalog, example); + if (entry == null) { + return "Example not found: " + example; + } + + List files = ExampleHelper.getFiles(entry); + if (!files.contains(file)) { + return "File '" + file + "' not found in example '" + example + "'. Available files: " + files; + } + + if (ExampleHelper.isBundled(entry)) { + String resourcePath = "examples/" + example + "/" + file; + try (InputStream is = ExampleHelper.class.getClassLoader().getResourceAsStream(resourcePath)) { + if (is != null) { + return IOHelper.loadText(is); + } + } catch (Exception e) { + return "Error reading file: " + e.getMessage(); + } + return "Could not read bundled file: " + resourcePath; + } else { + return "This example is not bundled. View it on GitHub: " + ExampleHelper.getGithubUrl(entry) + "/" + file; + } + } + + // ---- File tools ---- + + private String executeListFiles(JsonObject args) throws IOException { + String pathStr = args.getString("path"); + Path cwd = Path.of("").toAbsolutePath().normalize(); + Path base = cwd.resolve(pathStr != null && !pathStr.isBlank() ? pathStr : ".").normalize(); + + if (!base.startsWith(cwd)) { + return "Error: path must be within the current working directory"; + } + if (!Files.isDirectory(base)) { + return "Error: not a directory: " + cwd.relativize(base); + } + + try (Stream stream = Files.walk(base, 2)) { + List files = stream + .filter(p -> !p.equals(base)) + .map(p -> cwd.relativize(p).toString() + (Files.isDirectory(p) ? "/" : "")) + .sorted() + .toList(); + + JsonObject response = new JsonObject(); + response.put("directory", base.equals(cwd) ? "." : cwd.relativize(base).toString()); + response.put("count", files.size()); + response.put("files", files); + return response.toJson(); + } + } + + private String executeReadFile(JsonObject args) throws IOException { + String fileStr = args.getString("file"); + if (fileStr == null || fileStr.isBlank()) { + return "Error: file path is required"; + } + + Path cwd = Path.of("").toAbsolutePath().normalize(); + Path filePath = cwd.resolve(fileStr).normalize(); + + if (!filePath.startsWith(cwd)) { + return "Error: file path must be within the current working directory"; + } + if (!Files.exists(filePath)) { + return "File not found: " + cwd.relativize(filePath); + } + + String content = Files.readString(filePath); + if (content.length() > 32768) { + content = content.substring(0, 32768) + "\n... (truncated, file is " + content.length() + " bytes)"; + } + return content; + } + + private String executeWriteFile(JsonObject args) throws IOException { + String fileStr = args.getString("file"); + String content = args.getString("content"); + if (fileStr == null || fileStr.isBlank()) { + return "Error: file path is required"; + } + if (content == null) { + return "Error: content is required"; + } + + Path cwd = Path.of("").toAbsolutePath().normalize(); + Path filePath = cwd.resolve(fileStr).normalize(); + + if (!filePath.startsWith(cwd)) { + return "Error: file path must be within the current working directory"; + } + + Files.createDirectories(filePath.getParent()); + Files.writeString(filePath, content); + return "File written: " + cwd.relativize(filePath); + } + + private CamelCatalog getCatalog() { + if (catalog == null) { + catalog = new DefaultCamelCatalog(); + } + return catalog; + } + + // ---- JSON schema helpers for tool parameters ---- + + private static JsonObject emptyParams() { + JsonObject schema = new JsonObject(); + schema.put("type", "object"); + schema.put("properties", new JsonObject()); + return schema; + } + + private static JsonObject objectParams(Map properties) { + JsonObject props = new JsonObject(); + Map ordered = new LinkedHashMap<>(properties); + for (Map.Entry entry : ordered.entrySet()) { + props.put(entry.getKey(), entry.getValue()); + } + JsonObject schema = new JsonObject(); + schema.put("type", "object"); + schema.put("properties", props); + return schema; + } + + private static JsonObject stringProp(String description) { + JsonObject prop = new JsonObject(); + prop.put("type", "string"); + prop.put("description", description); + return prop; + } + + private static String truncate(String text, int maxLen) { + if (text == null) { + return "null"; + } + return text.length() <= maxLen ? text : text.substring(0, maxLen) + "..."; + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java index f766d4ce91c24..5d9035bcb1216 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java @@ -137,6 +137,7 @@ public void execute(String... args) { .addSubcommand("expression", new CommandLine(new EvalExpressionCommand(this)))) .addSubcommand("export", new CommandLine(new Export(this))) .addSubcommand("explain", new CommandLine(new Explain(this))) + .addSubcommand("ask", new CommandLine(new Ask(this))) .addSubcommand("harden", new CommandLine(new Harden(this))) .addSubcommand("get", new CommandLine(new CamelStatus(this)) .addSubcommand("bean", new CommandLine(new CamelBeanDump(this))) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CommandHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CommandHelper.java index 460ae9520cd1e..1bf2b16ad5e60 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CommandHelper.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CommandHelper.java @@ -17,6 +17,7 @@ package org.apache.camel.dsl.jbang.core.commands; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -26,6 +27,7 @@ import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.PathUtils; import org.apache.camel.dsl.jbang.core.common.Printer; +import org.jline.terminal.Terminal; public final class CommandHelper { @@ -93,8 +95,10 @@ public static boolean confirmOperation(String message, boolean yes) { System.out.print(message + " [y/N] "); System.out.flush(); try { - // Do not use try-with-resources here: closing the Scanner would close System.in - Scanner scanner = new Scanner(System.in); + Terminal terminal = EnvironmentHelper.getActiveTerminal(); + InputStream input = terminal != null ? terminal.input() : System.in; + // Do not use try-with-resources here: closing the Scanner would close the input stream + Scanner scanner = new Scanner(input); String answer = scanner.nextLine().trim().toLowerCase(); return "y".equals(answer) || "yes".equals(answer); } catch (Exception e) { @@ -116,8 +120,8 @@ public ReadConsoleTask(Runnable listener) { @Override public void run() { - if (System.console() != null) { - System.console().readLine(); + String line = EnvironmentHelper.readLine(); + if (line != null) { listener.run(); } } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java index c1b1b56bc8871..67812323163ab 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java @@ -17,7 +17,6 @@ package org.apache.camel.dsl.jbang.core.commands; import java.io.BufferedReader; -import java.io.Console; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -41,6 +40,7 @@ import org.apache.camel.dsl.jbang.core.commands.action.MessageTableHelper; import org.apache.camel.dsl.jbang.core.common.CamelCommandHelper; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.PathUtils; import org.apache.camel.dsl.jbang.core.common.ProcessHelper; import org.apache.camel.dsl.jbang.core.common.VersionHelper; @@ -187,7 +187,6 @@ public Integer doCall() throws Exception { // read log input final AtomicBoolean quit = new AtomicBoolean(); - final Console c = System.console(); if (logLines > 0) { Thread t = new Thread(() -> { doReadLog(quit); @@ -196,7 +195,7 @@ public Integer doCall() throws Exception { } // read CLI input from user - Thread t2 = new Thread(() -> doRead(c, quit), "ReadCommand"); + Thread t2 = new Thread(() -> doRead(quit), "ReadCommand"); t2.start(); do { @@ -285,9 +284,9 @@ private void doReadLog(AtomicBoolean quit) { } while (!quit.get()); } - private void doRead(Console c, AtomicBoolean quit) { + private void doRead(AtomicBoolean quit) { do { - String line = c.readLine(); + String line = EnvironmentHelper.readLine(); if (line != null) { line = line.trim(); if ("q".equalsIgnoreCase(line) || "quit".equalsIgnoreCase(line) || "exit".equalsIgnoreCase(line)) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Explain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Explain.java index 319f783da2a58..f55d479dd8420 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Explain.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Explain.java @@ -16,32 +16,18 @@ */ package org.apache.camel.dsl.jbang.core.commands; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpTimeoutException; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Duration; import java.util.Arrays; -import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.function.BiFunction; -import java.util.stream.Stream; import org.apache.camel.catalog.CamelCatalog; import org.apache.camel.catalog.DefaultCamelCatalog; -import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; import org.apache.camel.tooling.model.ComponentModel; import org.apache.camel.tooling.model.EipModel; import org.apache.camel.util.FileUtil; -import org.apache.camel.util.json.JsonArray; -import org.apache.camel.util.json.JsonObject; -import org.apache.camel.util.json.Jsoner; import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; @@ -49,7 +35,7 @@ /** * Command to explain Camel routes using AI/LLM services. *

- * Supports multiple LLM providers: Ollama, OpenAI, Azure OpenAI, vLLM, LM Studio, LocalAI, etc. + * Supports multiple LLM providers: Ollama, OpenAI, Azure OpenAI, Anthropic, Vertex AI, vLLM, LM Studio, LocalAI, etc. */ @Command(name = "explain", description = "Explain what a Camel route does using AI/LLM", @@ -58,7 +44,8 @@ "%nExamples:", " camel explain hello.java", " camel explain hello.yaml --format=markdown", - " camel explain hello.java --model=gpt-4" }) + " camel explain hello.java --model=gpt-4", + " camel explain hello.yaml --api-type=anthropic" }) public class Explain extends CamelCommand { public static class FormatCompletionCandidates implements Iterable { @@ -72,10 +59,7 @@ public Iterator iterator() { } } - private static final String DEFAULT_OLLAMA_URL = "http://localhost:11434"; private static final String DEFAULT_MODEL = "llama3.2"; - private static final int CONNECT_TIMEOUT_SECONDS = 10; - private static final int HEALTH_CHECK_TIMEOUT_SECONDS = 5; private static final List COMMON_COMPONENTS = Arrays.asList( "kafka", "http", "https", "file", "timer", "direct", "seda", @@ -100,21 +84,6 @@ public Iterator iterator() { "marshal", "unmarshal", "convertBodyTo", "enrich", "pollEnrich", "wireTap", "pipeline"); - enum ApiType { - ollama((explain, prompts) -> explain.callOllama(prompts[0], prompts[1], prompts[2])), - openai((explain, prompts) -> explain.callOpenAiCompatible(prompts[0], prompts[1], prompts[2], prompts[3])); - - private final BiFunction caller; - - ApiType(BiFunction caller) { - this.caller = caller; - } - - String call(Explain explain, String endpoint, String sysPrompt, String userPrompt, String apiKey) { - return caller.apply(explain, new String[] { endpoint, sysPrompt, userPrompt, apiKey }); - } - } - @Parameters(description = "Route file(s) to explain", arity = "1..*") List files; @@ -123,12 +92,11 @@ String call(Explain explain, String endpoint, String sysPrompt, String userPromp String url; @Option(names = { "--api-type" }, - description = "API type: 'ollama' or 'openai' (OpenAI-compatible)", - defaultValue = "ollama") - ApiType apiType = ApiType.ollama; + description = "API type: 'ollama', 'openai' (OpenAI-compatible), or 'anthropic' (Anthropic/Vertex AI)") + LlmClient.ApiType apiType; @Option(names = { "--api-key" }, - description = "API key for authentication. Also reads OPENAI_API_KEY or LLM_API_KEY env vars") + description = "API key for authentication. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY env vars") String apiKey; @Option(names = { "--model" }, @@ -173,27 +141,31 @@ String call(Explain explain, String endpoint, String sysPrompt, String userPromp defaultValue = "true") boolean stream = true; - private final HttpClient httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(CONNECT_TIMEOUT_SECONDS)) - .build(); - public Explain(CamelJBangMain main) { super(main); } @Override public Integer doCall() throws Exception { - String endpoint = detectEndpoint(); - if (endpoint == null) { + LlmClient client = LlmClient.create() + .withUrl(url) + .withApiType(apiType) + .withApiKey(apiKey) + .withModel(model) + .withTimeout(timeout) + .withTemperature(temperature) + .withStream(stream) + .withPrinter(printer()); + + if (!client.detectEndpoint()) { printUsageHelp(); return 1; } - String resolvedApiKey = resolveApiKey(); - printConfiguration(endpoint, resolvedApiKey); + printConfiguration(client); for (String file : files) { - int result = explainRoute(file, endpoint, resolvedApiKey); + int result = explainRoute(file, client); if (result != 0) { return result; } @@ -201,12 +173,12 @@ public Integer doCall() throws Exception { return 0; } - private void printConfiguration(String endpoint, String resolvedApiKey) { + private void printConfiguration(LlmClient client) { printer().println("LLM Configuration:"); - printer().println(" URL: " + endpoint); - printer().println(" API Type: " + apiType); - printer().println(" Model: " + model); - printMaskedApiKey(resolvedApiKey); + printer().println(" URL: " + (client.url != null ? client.url : "(Vertex AI)")); + printer().println(" API Type: " + client.apiType); + printer().println(" Model: " + client.model); + printMaskedApiKey(client.apiKey); printer().println(); } @@ -225,119 +197,10 @@ private void printUsageHelp() { printer().printErr(" 1. camel infra run ollama"); printer().printErr(" 2. camel explain my-route.yaml --url=http://localhost:11434"); printer().printErr(" 3. camel explain my-route.yaml --url=https://api.openai.com --api-type=openai --api-key=sk-..."); + printer().printErr(" 4. camel explain my-route.yaml --api-type=anthropic (uses ANTHROPIC_API_KEY or Vertex AI)"); } - private String detectEndpoint() { - return tryExplicitUrl() - .or(this::tryInfraOllama) - .or(this::tryDefaultOllama) - .orElse(null); - } - - private Optional tryExplicitUrl() { - if (url == null || url.isBlank()) { - return Optional.empty(); - } - if (isEndpointReachable(url)) { - return Optional.of(url); - } - printer().printErr("Cannot connect to LLM service at: " + url); - return Optional.empty(); - } - - private Optional tryInfraOllama() { - try { - Map pids = findOllamaPids(); - for (Path pidFile : pids.values()) { - String baseUrl = readBaseUrlFromPidFile(pidFile); - if (baseUrl != null && isEndpointReachable(baseUrl)) { - apiType = ApiType.ollama; - return Optional.of(baseUrl); - } - } - } catch (Exception e) { - // ignore - } - return Optional.empty(); - } - - private String readBaseUrlFromPidFile(Path pidFile) throws Exception { - String json = Files.readString(pidFile); - JsonObject jo = (JsonObject) Jsoner.deserialize(json); - return jo.getString("baseUrl"); - } - - private Optional tryDefaultOllama() { - if (isEndpointReachable(DEFAULT_OLLAMA_URL)) { - apiType = ApiType.ollama; - return Optional.of(DEFAULT_OLLAMA_URL); - } - return Optional.empty(); - } - - private String resolveApiKey() { - if (apiKey != null && !apiKey.isBlank()) { - return apiKey; - } - return Stream.of("OPENAI_API_KEY", "LLM_API_KEY") - .map(System::getenv) - .filter(k -> k != null && !k.isBlank()) - .findFirst() - .orElse(null); - } - - private Map findOllamaPids() throws Exception { - Map pids = new HashMap<>(); - Path camelDir = CommandLineHelper.getCamelDir(); - - if (!Files.exists(camelDir)) { - return pids; - } - - try (Stream fileStream = Files.list(camelDir)) { - fileStream - .filter(this::isOllamaPidFile) - .forEach(p -> addPidEntry(pids, p)); - } - return pids; - } - - private boolean isOllamaPidFile(Path p) { - String name = p.getFileName().toString(); - return name.startsWith("infra-ollama-") && name.endsWith(".json"); - } - - private void addPidEntry(Map pids, Path p) { - String name = p.getFileName().toString(); - String pidStr = name.substring(name.lastIndexOf("-") + 1, name.lastIndexOf('.')); - try { - pids.put(Long.valueOf(pidStr), p); - } catch (NumberFormatException e) { - // ignore - } - } - - private boolean isEndpointReachable(String endpoint) { - return tryHealthCheck(endpoint + "/api/tags") - || tryHealthCheck(endpoint + "/v1/models") - || tryHealthCheck(endpoint); - } - - private boolean tryHealthCheck(String healthUrl) { - try { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(healthUrl)) - .timeout(Duration.ofSeconds(HEALTH_CHECK_TIMEOUT_SECONDS)) - .GET() - .build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - return response.statusCode() == 200; - } catch (Exception e) { - return false; - } - } - - private int explainRoute(String file, String endpoint, String resolvedApiKey) throws Exception { + private int explainRoute(String file, LlmClient client) throws Exception { Path path = Path.of(file); if (!Files.exists(path)) { printer().printErr("File not found: " + file); @@ -354,7 +217,10 @@ private int explainRoute(String file, String endpoint, String resolvedApiKey) th printPromptsIfRequested(sysPrompt, userPrompt); - String explanation = apiType.call(this, endpoint, sysPrompt, userPrompt, resolvedApiKey); + printer().println("Analyzing route with " + client.model + " (" + client.apiType + ")..."); + printer().println(); + + String explanation = client.generate(sysPrompt, userPrompt); return handleExplanationResult(explanation); } @@ -383,8 +249,6 @@ private int handleExplanationResult(String explanation) { printer().printErr("Failed to get explanation from LLM"); return 1; } - // With streaming, response was already printed during generation - // Without streaming, we need to print it now if (!stream) { printer().println(explanation); } @@ -496,159 +360,4 @@ private String camelCaseToDash(String text) { } return sb.toString(); } - - String callOllama(String endpoint, String sysPrompt, String userPrompt) { - JsonObject request = new JsonObject(); - request.put("model", model); - request.put("prompt", userPrompt); - request.put("system", sysPrompt); - request.put("stream", stream); - - JsonObject options = new JsonObject(); - options.put("temperature", temperature); - request.put("options", options); - - printer().println("Analyzing route with " + model + " (Ollama)..."); - printer().println(); - - if (stream) { - return sendStreamingRequest(endpoint + "/api/generate", request); - } - JsonObject response = sendRequest(endpoint + "/api/generate", request, null); - return response != null ? response.getString("response") : null; - } - - String callOpenAiCompatible(String endpoint, String sysPrompt, String userPrompt, String resolvedApiKey) { - JsonArray messages = new JsonArray(); - messages.add(createMessage("system", sysPrompt)); - messages.add(createMessage("user", userPrompt)); - - JsonObject request = new JsonObject(); - request.put("model", model); - request.put("messages", messages); - request.put("temperature", temperature); - - String apiUrl = normalizeOpenAiUrl(endpoint); - - printer().println("Analyzing route with " + model + " (OpenAI-compatible)..."); - printer().println(); - - JsonObject response = sendRequest(apiUrl, request, resolvedApiKey); - return extractOpenAiContent(response); - } - - private JsonObject createMessage(String role, String content) { - JsonObject msg = new JsonObject(); - msg.put("role", role); - msg.put("content", content); - return msg; - } - - private String normalizeOpenAiUrl(String endpoint) { - String url = endpoint.endsWith("/") ? endpoint.substring(0, endpoint.length() - 1) : endpoint; - if (!url.endsWith("/v1/chat/completions")) { - url = url.endsWith("/v1") ? url : url + "/v1"; - url = url + "/chat/completions"; - } - return url; - } - - private String extractOpenAiContent(JsonObject response) { - if (response == null) { - return null; - } - JsonArray choices = (JsonArray) response.get("choices"); - if (choices == null || choices.isEmpty()) { - return null; - } - JsonObject firstChoice = (JsonObject) choices.get(0); - JsonObject message = (JsonObject) firstChoice.get("message"); - return message != null ? message.getString("content") : null; - } - - private String sendStreamingRequest(String url, JsonObject body) { - try { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .timeout(Duration.ofSeconds(timeout)) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(body.toJson())) - .build(); - - HttpResponse> response = httpClient.send( - request, HttpResponse.BodyHandlers.ofLines()); - - if (response.statusCode() != 200) { - handleErrorStatus(response.statusCode(), "Streaming request failed"); - return null; - } - - StringBuilder fullResponse = new StringBuilder(); - response.body().forEach(line -> { - if (line.isBlank()) { - return; - } - try { - JsonObject chunk = (JsonObject) Jsoner.deserialize(line); - String text = chunk.getString("response"); - if (text != null) { - printer().print(text); - fullResponse.append(text); - } - } catch (Exception e) { - // Skip malformed chunks - } - }); - - printer().println(); - return fullResponse.toString(); - - } catch (HttpTimeoutException e) { - printer().printErr("\nRequest timed out after " + timeout + " seconds."); - return null; - } catch (Exception e) { - printer().printErr("\nError during streaming: " + e.getMessage()); - return null; - } - } - - private JsonObject sendRequest(String url, JsonObject body, String authKey) { - try { - HttpRequest.Builder builder = HttpRequest.newBuilder() - .uri(URI.create(url)) - .timeout(Duration.ofSeconds(timeout)) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(body.toJson())); - - if (authKey != null && !authKey.isBlank()) { - builder.header("Authorization", "Bearer " + authKey); - } - - HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() == 200) { - return (JsonObject) Jsoner.deserialize(response.body()); - } - - handleErrorStatus(response.statusCode(), response.body()); - return null; - - } catch (HttpTimeoutException e) { - printer().printErr("Request timed out after " + timeout + " seconds."); - return null; - } catch (Exception e) { - printer().printErr("Error calling LLM: " + e.getMessage()); - return null; - } - } - - private void handleErrorStatus(int statusCode, String body) { - printer().printErr("LLM returned status: " + statusCode); - switch (statusCode) { - case 401 -> printer().printErr("Authentication failed. Check your API key."); - case 404 -> printer().printErr("Model '" + model + "' not found."); - case 429 -> printer().printErr("Rate limit exceeded."); - default -> printer().printErr(body); - } - } } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java index 00772ae1cfa47..da2d911daf109 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java @@ -34,6 +34,7 @@ import org.apache.camel.CamelContext; import org.apache.camel.dsl.jbang.core.commands.catalog.KameletCatalogHelper; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.ResourceDoesNotExist; import org.apache.camel.dsl.jbang.core.common.VersionHelper; import org.apache.camel.github.GistResourceResolver; @@ -43,6 +44,7 @@ import org.apache.camel.util.FileUtil; import org.apache.camel.util.IOHelper; import org.apache.commons.io.IOUtils; +import org.jline.terminal.Terminal; import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; @@ -106,7 +108,7 @@ public Integer doCall() throws Exception { } if (file == null) { // try interactive picker if running in a TTY and not in CI - if (System.console() != null && System.getenv("CI") == null) { + if (EnvironmentHelper.isInteractiveTerminal()) { return interactivePicker(); } printer().printErr("Missing required parameter: "); @@ -309,7 +311,9 @@ private int interactivePicker() throws Exception { pipeTemplates.add(new String[] { "init-pipe.yaml", "Pipe CR (source to sink)", ".yaml" }); categories.put("Pipes and CRs", pipeTemplates); - Scanner scanner = new Scanner(System.in); + Terminal activeTerminal = EnvironmentHelper.getActiveTerminal(); + InputStream scannerInput = activeTerminal != null ? activeTerminal.input() : System.in; + Scanner scanner = new Scanner(scannerInput); // Step 1: Pick a category printer().println("Select a template category:"); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java new file mode 100644 index 0000000000000..85e2829240489 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java @@ -0,0 +1,1238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpTimeoutException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.camel.dsl.jbang.core.common.Printer; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; +import org.apache.camel.util.json.Jsoner; + +/** + * Shared LLM HTTP client supporting Ollama, OpenAI-compatible, and Anthropic (including Vertex AI) APIs. + */ +class LlmClient { + + private static final String DEFAULT_OLLAMA_URL = "http://localhost:11434"; + private static final String DEFAULT_ANTHROPIC_URL = "https://api.anthropic.com"; + private static final String ANTHROPIC_VERSION = "2023-06-01"; + private static final String VERTEX_ANTHROPIC_VERSION = "vertex-2023-10-16"; + private static final String DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-6"; + private static final int CONNECT_TIMEOUT_SECONDS = 10; + private static final int HEALTH_CHECK_TIMEOUT_SECONDS = 5; + + enum ApiType { + ollama, + openai, + anthropic + } + + // -- Unified abstractions for tool-calling across API formats -- + + record ToolDef(String name, String description, JsonObject parameters) { + } + + record ToolCall(String id, String name, JsonObject arguments) { + } + + record ToolResult(String toolCallId, String content) { + } + + record Message(String role, String content, List toolCalls, List toolResults) { + + static Message user(String text) { + return new Message("user", text, null, null); + } + + static Message assistantWithToolCalls(String text, List calls) { + return new Message("assistant", text, calls, null); + } + + static Message toolResults(List results) { + return new Message("tool", null, null, results); + } + } + + record ChatResponse(String text, List toolCalls, String stopReason, boolean streamed) { + } + + // -- Configuration -- + + ApiType apiType; + String url; + String apiKey; + String model; + int timeout; + double temperature; + boolean stream; + int maxTokens; + boolean verbose; + Printer printer; + + private final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(CONNECT_TIMEOUT_SECONDS)) + .build(); + + // Vertex AI specific + private String vertexRegion; + private String vertexProjectId; + + // -- Builder -- + + static LlmClient create() { + return new LlmClient(); + } + + LlmClient withApiType(ApiType apiType) { + this.apiType = apiType; + return this; + } + + LlmClient withUrl(String url) { + this.url = url; + return this; + } + + LlmClient withApiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + LlmClient withModel(String model) { + this.model = model; + return this; + } + + LlmClient withTimeout(int timeout) { + this.timeout = timeout; + return this; + } + + LlmClient withTemperature(double temperature) { + this.temperature = temperature; + return this; + } + + LlmClient withStream(boolean stream) { + this.stream = stream; + return this; + } + + LlmClient withMaxTokens(int maxTokens) { + this.maxTokens = maxTokens; + return this; + } + + LlmClient withVerbose(boolean verbose) { + this.verbose = verbose; + return this; + } + + LlmClient withPrinter(Printer printer) { + this.printer = printer; + return this; + } + + // -- Auto-detection -- + + boolean detectEndpoint() { + boolean found; + if (tryExplicitUrl()) { + found = true; + } else if (apiType != null) { + found = switch (apiType) { + case anthropic -> tryAnthropicOrVertex(); + case openai -> tryOpenAi(); + case ollama -> tryInfraOllama() || tryDefaultOllama(); + }; + } else { + // auto-detect priority: anthropic → vertex → openai → ollama + found = tryAnthropicApiKey() + || tryVertexAi() + || tryOpenAi() + || tryInfraOllama() + || tryDefaultOllama(); + } + if (found && apiType == ApiType.ollama && "llama3.2".equals(model)) { + resolveOllamaModel(); + } + return found; + } + + String resolveApiKey() { + if (apiKey != null && !apiKey.isBlank()) { + return apiKey; + } + if (apiType == ApiType.anthropic) { + String key = System.getenv("ANTHROPIC_API_KEY"); + if (key != null && !key.isBlank()) { + apiKey = key; + return key; + } + // Vertex AI uses gcloud token, not API key + return null; + } + return Stream.of("OPENAI_API_KEY", "LLM_API_KEY") + .map(System::getenv) + .filter(k -> k != null && !k.isBlank()) + .findFirst() + .map(k -> { + apiKey = k; + return k; + }) + .orElse(null); + } + + // -- Simple generate (for explain) -- + + String generate(String systemPrompt, String userPrompt) { + return switch (apiType) { + case ollama -> generateOllama(systemPrompt, userPrompt); + case openai -> generateOpenAi(systemPrompt, userPrompt); + case anthropic -> generateAnthropic(systemPrompt, userPrompt); + }; + } + + // -- Chat with tools (for ask) -- + + ChatResponse chatWithTools(String systemPrompt, List messages, List tools) { + return switch (apiType) { + case ollama -> chatOllamaFormat(systemPrompt, messages, tools); + case openai -> chatOpenAiFormat(systemPrompt, messages, tools); + case anthropic -> chatAnthropicFormat(systemPrompt, messages, tools); + }; + } + + // ---- Ollama generate ---- + + private String generateOllama(String systemPrompt, String userPrompt) { + JsonObject request = new JsonObject(); + request.put("model", model); + request.put("prompt", userPrompt); + request.put("system", systemPrompt); + request.put("stream", stream); + + JsonObject options = new JsonObject(); + options.put("temperature", temperature); + request.put("options", options); + + if (stream) { + return sendStreamingRequest(url + "/api/generate", request, null, "response"); + } + JsonObject response = sendRequest(url + "/api/generate", request, null); + return response != null ? response.getString("response") : null; + } + + // ---- OpenAI-compatible generate ---- + + private String generateOpenAi(String systemPrompt, String userPrompt) { + JsonArray messages = new JsonArray(); + messages.add(createOpenAiMessage("system", systemPrompt)); + messages.add(createOpenAiMessage("user", userPrompt)); + + JsonObject request = new JsonObject(); + request.put("model", model); + request.put("messages", messages); + request.put("temperature", temperature); + + String resolvedKey = resolveApiKey(); + String apiUrl = normalizeOpenAiUrl(url); + + JsonObject response = sendRequest(apiUrl, request, resolvedKey); + return extractOpenAiContent(response); + } + + // ---- Anthropic generate ---- + + private String generateAnthropic(String systemPrompt, String userPrompt) { + JsonArray messages = new JsonArray(); + JsonObject userMsg = new JsonObject(); + userMsg.put("role", "user"); + JsonArray content = new JsonArray(); + JsonObject textBlock = new JsonObject(); + textBlock.put("type", "text"); + textBlock.put("text", userPrompt); + content.add(textBlock); + userMsg.put("content", content); + messages.add(userMsg); + + JsonObject request = buildAnthropicRequest(systemPrompt, messages, null); + + String apiUrl = resolveAnthropicUrl(); + Map headers = buildAnthropicHeaders(); + + if (stream) { + return sendAnthropicStreamingRequest(apiUrl, request, headers); + } + JsonObject response = sendRequestWithHeaders(apiUrl, request, headers); + return extractAnthropicTextContent(response); + } + + // ---- OpenAI/Ollama chat with tools ---- + + private ChatResponse chatOpenAiFormat(String systemPrompt, List messages, List tools) { + JsonArray jsonMessages = buildChatMessages(systemPrompt, messages, false); + JsonArray jsonTools = buildOpenAiStyleTools(tools); + + JsonObject request = new JsonObject(); + request.put("model", model); + request.put("messages", jsonMessages); + request.put("temperature", temperature); + if (jsonTools != null) { + request.put("tools", jsonTools); + } + + String apiUrl = normalizeOpenAiUrl(url); + String resolvedKey = resolveApiKey(); + + JsonObject response = sendRequest(apiUrl, request, resolvedKey); + return parseOpenAiChatResponse(response); + } + + // ---- Ollama native chat with tools ---- + + private ChatResponse chatOllamaFormat(String systemPrompt, List messages, List tools) { + JsonArray jsonMessages = buildChatMessages(systemPrompt, messages, true); + JsonArray jsonTools = buildOpenAiStyleTools(tools); + + JsonObject request = new JsonObject(); + request.put("model", model); + request.put("messages", jsonMessages); + request.put("think", false); + if (jsonTools != null) { + request.put("tools", jsonTools); + } + + JsonObject options = new JsonObject(); + options.put("temperature", temperature); + request.put("options", options); + + if (stream) { + request.put("stream", true); + return streamOllamaChatRequest(url + "/api/chat", request); + } + request.put("stream", false); + JsonObject response = sendRequest(url + "/api/chat", request, null); + return parseOllamaChatResponse(response); + } + + private ChatResponse streamOllamaChatRequest(String requestUrl, JsonObject body) { + try { + String jsonBody = body.toJson(); + if (verbose) { + printer.println("[verbose] POST (streaming) " + requestUrl); + printer.println("[verbose] Request: " + truncateVerbose(jsonBody)); + } + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(requestUrl)) + .timeout(Duration.ofSeconds(timeout)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)); + + HttpResponse> response = httpClient.send( + builder.build(), HttpResponse.BodyHandlers.ofLines()); + + if (response.statusCode() != 200) { + handleErrorStatus(response.statusCode(), "Streaming request failed"); + return new ChatResponse(null, List.of(), "error", false); + } + + StringBuilder fullText = new StringBuilder(); + List toolCalls = new ArrayList<>(); + String[] doneReasonHolder = { null }; + + response.body().forEach(line -> { + if (line.isBlank()) { + return; + } + if (verbose) { + printer.println("[verbose] Chunk: " + truncateVerbose(line)); + } + try { + JsonObject chunk = (JsonObject) Jsoner.deserialize(line); + JsonObject message = (JsonObject) chunk.get("message"); + if (message == null) { + return; + } + + String content = message.getString("content"); + if (content != null && !content.isEmpty()) { + printer.print(content); + fullText.append(content); + } + + JsonArray rawToolCalls = (JsonArray) message.get("tool_calls"); + if (rawToolCalls != null) { + int idx = toolCalls.size(); + for (Object obj : rawToolCalls) { + JsonObject tc = (JsonObject) obj; + JsonObject function = (JsonObject) tc.get("function"); + String id = tc.containsKey("id") ? tc.getString("id") : "call_" + idx; + String name = function.getString("name"); + JsonObject args; + Object argsObj = function.get("arguments"); + if (argsObj instanceof JsonObject jo) { + args = jo; + } else if (argsObj instanceof String s) { + try { + args = (JsonObject) Jsoner.deserialize(s); + } catch (Exception e) { + args = new JsonObject(); + } + } else { + args = new JsonObject(); + } + toolCalls.add(new ToolCall(id, name, args)); + idx++; + } + } + + if (Boolean.TRUE.equals(chunk.get("done"))) { + doneReasonHolder[0] = chunk.getString("done_reason"); + } + } catch (Exception e) { + // skip malformed chunks + } + }); + + if (fullText.length() > 0) { + printer.println(); + } + + String text = fullText.length() > 0 ? fullText.toString() : null; + String stopReason + = !toolCalls.isEmpty() ? "tool_calls" : (doneReasonHolder[0] != null ? doneReasonHolder[0] : "stop"); + + if (verbose) { + printer.println("[verbose] Streamed Ollama: text=" + (text != null ? truncateVerbose(text) : "null") + + ", toolCalls=" + toolCalls.size() + ", doneReason=" + doneReasonHolder[0]); + } + return new ChatResponse(text, toolCalls, stopReason, true); + } catch (HttpTimeoutException e) { + printer.println("\nRequest timed out after " + timeout + " seconds."); + return new ChatResponse(null, List.of(), "error", false); + } catch (Exception e) { + printer.println("\nError during streaming: " + e.getMessage()); + return new ChatResponse(null, List.of(), "error", false); + } + } + + // ---- Shared message/tool builders ---- + + private JsonArray buildChatMessages(String systemPrompt, List messages, boolean ollamaNative) { + JsonArray jsonMessages = new JsonArray(); + jsonMessages.add(createOpenAiMessage("system", systemPrompt)); + + for (Message msg : messages) { + if (msg.toolCalls() != null && !msg.toolCalls().isEmpty()) { + JsonObject assistantMsg = new JsonObject(); + assistantMsg.put("role", "assistant"); + if (msg.content() != null) { + assistantMsg.put("content", msg.content()); + } + JsonArray toolCalls = new JsonArray(); + for (ToolCall tc : msg.toolCalls()) { + JsonObject call = new JsonObject(); + if (!ollamaNative) { + call.put("id", tc.id()); + call.put("type", "function"); + } + JsonObject function = new JsonObject(); + function.put("name", tc.name()); + function.put("arguments", ollamaNative ? tc.arguments() : tc.arguments().toJson()); + call.put("function", function); + toolCalls.add(call); + } + assistantMsg.put("tool_calls", toolCalls); + jsonMessages.add(assistantMsg); + } else if (msg.toolResults() != null && !msg.toolResults().isEmpty()) { + for (ToolResult tr : msg.toolResults()) { + JsonObject toolMsg = new JsonObject(); + toolMsg.put("role", "tool"); + if (!ollamaNative) { + toolMsg.put("tool_call_id", tr.toolCallId()); + } + toolMsg.put("content", tr.content()); + jsonMessages.add(toolMsg); + } + } else { + jsonMessages.add(createOpenAiMessage(msg.role(), msg.content())); + } + } + return jsonMessages; + } + + private JsonArray buildOpenAiStyleTools(List tools) { + if (tools == null || tools.isEmpty()) { + return null; + } + JsonArray jsonTools = new JsonArray(); + for (ToolDef tool : tools) { + JsonObject toolObj = new JsonObject(); + toolObj.put("type", "function"); + JsonObject function = new JsonObject(); + function.put("name", tool.name()); + function.put("description", tool.description()); + function.put("parameters", tool.parameters()); + toolObj.put("function", function); + jsonTools.add(toolObj); + } + return jsonTools; + } + + // ---- Anthropic chat with tools ---- + + private ChatResponse chatAnthropicFormat(String systemPrompt, List messages, List tools) { + JsonArray jsonMessages = new JsonArray(); + + for (Message msg : messages) { + if (msg.toolCalls() != null && !msg.toolCalls().isEmpty()) { + // assistant message with tool_use blocks + JsonObject assistantMsg = new JsonObject(); + assistantMsg.put("role", "assistant"); + JsonArray content = new JsonArray(); + if (msg.content() != null) { + JsonObject textBlock = new JsonObject(); + textBlock.put("type", "text"); + textBlock.put("text", msg.content()); + content.add(textBlock); + } + for (ToolCall tc : msg.toolCalls()) { + JsonObject toolUse = new JsonObject(); + toolUse.put("type", "tool_use"); + toolUse.put("id", tc.id()); + toolUse.put("name", tc.name()); + toolUse.put("input", tc.arguments()); + content.add(toolUse); + } + assistantMsg.put("content", content); + jsonMessages.add(assistantMsg); + } else if (msg.toolResults() != null && !msg.toolResults().isEmpty()) { + // tool results as user message with tool_result blocks + JsonObject userMsg = new JsonObject(); + userMsg.put("role", "user"); + JsonArray content = new JsonArray(); + for (ToolResult tr : msg.toolResults()) { + JsonObject toolResult = new JsonObject(); + toolResult.put("type", "tool_result"); + toolResult.put("tool_use_id", tr.toolCallId()); + toolResult.put("content", tr.content()); + content.add(toolResult); + } + userMsg.put("content", content); + jsonMessages.add(userMsg); + } else { + JsonObject m = new JsonObject(); + m.put("role", msg.role()); + JsonArray content = new JsonArray(); + JsonObject textBlock = new JsonObject(); + textBlock.put("type", "text"); + textBlock.put("text", msg.content()); + content.add(textBlock); + m.put("content", content); + jsonMessages.add(m); + } + } + + JsonArray jsonTools = null; + if (tools != null && !tools.isEmpty()) { + jsonTools = new JsonArray(); + for (ToolDef tool : tools) { + JsonObject toolObj = new JsonObject(); + toolObj.put("name", tool.name()); + toolObj.put("description", tool.description()); + toolObj.put("input_schema", tool.parameters()); + jsonTools.add(toolObj); + } + } + + JsonObject request = buildAnthropicRequest(systemPrompt, jsonMessages, jsonTools); + String apiUrl = resolveAnthropicUrl(); + Map headers = buildAnthropicHeaders(); + + JsonObject response = sendRequestWithHeaders(apiUrl, request, headers); + return parseAnthropicChatResponse(response); + } + + // ---- Anthropic helpers ---- + + private JsonObject buildAnthropicRequest(String systemPrompt, JsonArray messages, JsonArray tools) { + JsonObject request = new JsonObject(); + if (isVertexAi()) { + // Vertex AI: model is in the URL, version goes in body + request.put("anthropic_version", VERTEX_ANTHROPIC_VERSION); + } else { + // Direct Anthropic API: model goes in body + request.put("model", model); + } + request.put("max_tokens", maxTokens > 0 ? maxTokens : 4096); + if (systemPrompt != null) { + request.put("system", systemPrompt); + } + request.put("messages", messages); + request.put("temperature", temperature); + if (tools != null && !tools.isEmpty()) { + request.put("tools", tools); + } + return request; + } + + private String resolveAnthropicUrl() { + if (isVertexAi()) { + return String.format( + "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/anthropic/models/%s:rawPredict", + vertexRegion, vertexProjectId, vertexRegion, model); + } + String base = url != null ? url : DEFAULT_ANTHROPIC_URL; + if (base.endsWith("/")) { + base = base.substring(0, base.length() - 1); + } + return base + "/v1/messages"; + } + + private Map buildAnthropicHeaders() { + Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json"); + if (isVertexAi()) { + String token = getGcloudAccessToken(); + if (token != null) { + headers.put("Authorization", "Bearer " + token); + } + } else { + String key = resolveApiKey(); + if (key != null) { + headers.put("x-api-key", key); + } + headers.put("anthropic-version", ANTHROPIC_VERSION); + } + return headers; + } + + private boolean isVertexAi() { + return vertexRegion != null && vertexProjectId != null; + } + + private String getGcloudAccessToken() { + try { + ProcessBuilder pb = new ProcessBuilder("gcloud", "auth", "print-access-token"); + pb.redirectErrorStream(true); + Process p = pb.start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) { + String token = reader.readLine(); + int exit = p.waitFor(); + if (exit == 0 && token != null && !token.isBlank()) { + return token.strip(); + } + } + } catch (Exception e) { + // gcloud not available + } + return null; + } + + // ---- Response parsing ---- + + private ChatResponse parseOpenAiChatResponse(JsonObject response) { + if (response == null) { + if (verbose) { + printer.println("[verbose] parseOpenAiChatResponse: response is null"); + } + return new ChatResponse(null, List.of(), "error", false); + } + JsonArray choices = (JsonArray) response.get("choices"); + if (choices == null || choices.isEmpty()) { + if (verbose) { + printer.println("[verbose] parseOpenAiChatResponse: no choices in response. Keys: " + response.keySet()); + } + return new ChatResponse(null, List.of(), "error", false); + } + JsonObject firstChoice = (JsonObject) choices.get(0); + String finishReason = firstChoice.getString("finish_reason"); + JsonObject message = (JsonObject) firstChoice.get("message"); + if (message == null) { + return new ChatResponse(null, List.of(), finishReason, false); + } + + String content = message.getString("content"); + if (content == null || content.isBlank()) { + String reasoning = message.getString("reasoning_content"); + if (reasoning == null || reasoning.isBlank()) { + reasoning = message.getString("reasoning"); + } + if (reasoning != null && !reasoning.isBlank()) { + content = reasoning; + } + } + JsonArray rawToolCalls = (JsonArray) message.get("tool_calls"); + List toolCalls = new ArrayList<>(); + if (rawToolCalls != null) { + for (Object obj : rawToolCalls) { + JsonObject tc = (JsonObject) obj; + JsonObject function = (JsonObject) tc.get("function"); + String id = tc.getString("id"); + String name = function.getString("name"); + JsonObject args; + try { + args = (JsonObject) Jsoner.deserialize(function.getString("arguments")); + } catch (Exception e) { + args = new JsonObject(); + } + toolCalls.add(new ToolCall(id, name, args)); + } + } + if (verbose) { + printer.println("[verbose] Parsed: text=" + (content != null ? truncateVerbose(content) : "null") + + ", toolCalls=" + toolCalls.size() + ", finishReason=" + finishReason); + } + return new ChatResponse(content, toolCalls, finishReason, false); + } + + private ChatResponse parseOllamaChatResponse(JsonObject response) { + if (response == null) { + if (verbose) { + printer.println("[verbose] parseOllamaChatResponse: response is null"); + } + return new ChatResponse(null, List.of(), "error", false); + } + JsonObject message = (JsonObject) response.get("message"); + if (message == null) { + if (verbose) { + printer.println("[verbose] parseOllamaChatResponse: no message in response. Keys: " + response.keySet()); + } + return new ChatResponse(null, List.of(), "error", false); + } + + String content = message.getString("content"); + String doneReason = response.getString("done_reason"); + + JsonArray rawToolCalls = (JsonArray) message.get("tool_calls"); + List toolCalls = new ArrayList<>(); + if (rawToolCalls != null) { + int idx = 0; + for (Object obj : rawToolCalls) { + JsonObject tc = (JsonObject) obj; + JsonObject function = (JsonObject) tc.get("function"); + String id = tc.containsKey("id") ? tc.getString("id") : "call_" + idx; + String name = function.getString("name"); + JsonObject args; + Object argsObj = function.get("arguments"); + if (argsObj instanceof JsonObject jo) { + args = jo; + } else if (argsObj instanceof String s) { + try { + args = (JsonObject) Jsoner.deserialize(s); + } catch (Exception e) { + args = new JsonObject(); + } + } else { + args = new JsonObject(); + } + toolCalls.add(new ToolCall(id, name, args)); + idx++; + } + } + + String stopReason = !toolCalls.isEmpty() ? "tool_calls" : (doneReason != null ? doneReason : "stop"); + + if (verbose) { + printer.println("[verbose] Parsed Ollama: text=" + (content != null ? truncateVerbose(content) : "null") + + ", toolCalls=" + toolCalls.size() + ", doneReason=" + doneReason); + } + return new ChatResponse(content, toolCalls, stopReason, false); + } + + private ChatResponse parseAnthropicChatResponse(JsonObject response) { + if (response == null) { + return new ChatResponse(null, List.of(), "error", false); + } + String stopReason = response.getString("stop_reason"); + JsonArray contentBlocks = (JsonArray) response.get("content"); + if (contentBlocks == null) { + return new ChatResponse(null, List.of(), stopReason, false); + } + + StringBuilder text = new StringBuilder(); + List toolCalls = new ArrayList<>(); + for (Object obj : contentBlocks) { + JsonObject block = (JsonObject) obj; + String type = block.getString("type"); + if ("text".equals(type)) { + text.append(block.getString("text")); + } else if ("tool_use".equals(type)) { + String id = block.getString("id"); + String name = block.getString("name"); + JsonObject input = (JsonObject) block.get("input"); + toolCalls.add(new ToolCall(id, name, input != null ? input : new JsonObject())); + } + } + String textContent = text.length() > 0 ? text.toString() : null; + return new ChatResponse(textContent, toolCalls, stopReason, false); + } + + private String extractOpenAiContent(JsonObject response) { + if (response == null) { + return null; + } + JsonArray choices = (JsonArray) response.get("choices"); + if (choices == null || choices.isEmpty()) { + return null; + } + JsonObject firstChoice = (JsonObject) choices.get(0); + JsonObject message = (JsonObject) firstChoice.get("message"); + return message != null ? message.getString("content") : null; + } + + private String extractAnthropicTextContent(JsonObject response) { + if (response == null) { + return null; + } + JsonArray contentBlocks = (JsonArray) response.get("content"); + if (contentBlocks == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (Object obj : contentBlocks) { + JsonObject block = (JsonObject) obj; + if ("text".equals(block.getString("type"))) { + sb.append(block.getString("text")); + } + } + return sb.length() > 0 ? sb.toString() : null; + } + + // ---- HTTP transport ---- + + private JsonObject sendRequest(String requestUrl, JsonObject body, String authKey) { + try { + String jsonBody = body.toJson(); + if (verbose) { + printer.println("[verbose] POST " + requestUrl); + printer.println("[verbose] Request: " + truncateVerbose(jsonBody)); + } + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(requestUrl)) + .timeout(Duration.ofSeconds(timeout)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)); + + if (authKey != null && !authKey.isBlank()) { + builder.header("Authorization", "Bearer " + authKey); + } + + HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + + if (verbose) { + printer.println("[verbose] Response status: " + response.statusCode()); + printer.println("[verbose] Response: " + truncateVerbose(response.body())); + } + + if (response.statusCode() == 200) { + return (JsonObject) Jsoner.deserialize(response.body()); + } + + handleErrorStatus(response.statusCode(), response.body()); + return null; + } catch (HttpTimeoutException e) { + printer.println("Request timed out after " + timeout + " seconds."); + return null; + } catch (Exception e) { + printer.println("Error calling LLM: " + e.getMessage()); + return null; + } + } + + private JsonObject sendRequestWithHeaders(String requestUrl, JsonObject body, Map headers) { + try { + String jsonBody = body.toJson(); + if (verbose) { + printer.println("[verbose] POST " + requestUrl); + printer.println("[verbose] Request: " + truncateVerbose(jsonBody)); + } + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(requestUrl)) + .timeout(Duration.ofSeconds(timeout)) + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)); + + for (Map.Entry h : headers.entrySet()) { + builder.header(h.getKey(), h.getValue()); + } + + HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + + if (verbose) { + printer.println("[verbose] Response status: " + response.statusCode()); + printer.println("[verbose] Response: " + truncateVerbose(response.body())); + } + + if (response.statusCode() == 200) { + return (JsonObject) Jsoner.deserialize(response.body()); + } + + handleErrorStatus(response.statusCode(), response.body()); + return null; + } catch (HttpTimeoutException e) { + printer.println("Request timed out after " + timeout + " seconds."); + return null; + } catch (Exception e) { + printer.println("Error calling LLM: " + e.getMessage()); + return null; + } + } + + String sendStreamingRequest(String requestUrl, JsonObject body, String authKey, String textField) { + try { + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(requestUrl)) + .timeout(Duration.ofSeconds(timeout)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body.toJson())); + + if (authKey != null && !authKey.isBlank()) { + builder.header("Authorization", "Bearer " + authKey); + } + + HttpResponse> response = httpClient.send( + builder.build(), HttpResponse.BodyHandlers.ofLines()); + + if (response.statusCode() != 200) { + handleErrorStatus(response.statusCode(), "Streaming request failed"); + return null; + } + + StringBuilder fullResponse = new StringBuilder(); + response.body().forEach(line -> { + if (line.isBlank()) { + return; + } + try { + JsonObject chunk = (JsonObject) Jsoner.deserialize(line); + String text = chunk.getString(textField); + if (text != null) { + printer.print(text); + fullResponse.append(text); + } + } catch (Exception e) { + // skip malformed chunks + } + }); + printer.println(); + return fullResponse.toString(); + } catch (HttpTimeoutException e) { + printer.println("\nRequest timed out after " + timeout + " seconds."); + return null; + } catch (Exception e) { + printer.println("\nError during streaming: " + e.getMessage()); + return null; + } + } + + private String sendAnthropicStreamingRequest(String requestUrl, JsonObject body, Map headers) { + body.put("stream", true); + try { + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(requestUrl)) + .timeout(Duration.ofSeconds(timeout)) + .POST(HttpRequest.BodyPublishers.ofString(body.toJson())); + + for (Map.Entry h : headers.entrySet()) { + builder.header(h.getKey(), h.getValue()); + } + + HttpResponse> response = httpClient.send( + builder.build(), HttpResponse.BodyHandlers.ofLines()); + + if (response.statusCode() != 200) { + handleErrorStatus(response.statusCode(), "Streaming request failed"); + return null; + } + + StringBuilder fullResponse = new StringBuilder(); + response.body().forEach(line -> { + if (line.isBlank() || !line.startsWith("data: ")) { + return; + } + String data = line.substring(6); + if ("[DONE]".equals(data)) { + return; + } + try { + JsonObject event = (JsonObject) Jsoner.deserialize(data); + String type = event.getString("type"); + if ("content_block_delta".equals(type)) { + JsonObject delta = (JsonObject) event.get("delta"); + if (delta != null && "text_delta".equals(delta.getString("type"))) { + String text = delta.getString("text"); + if (text != null) { + printer.print(text); + fullResponse.append(text); + } + } + } + } catch (Exception e) { + // skip malformed events + } + }); + printer.println(); + return fullResponse.toString(); + } catch (HttpTimeoutException e) { + printer.println("\nRequest timed out after " + timeout + " seconds."); + return null; + } catch (Exception e) { + printer.println("\nError during streaming: " + e.getMessage()); + return null; + } + } + + // ---- Endpoint detection helpers ---- + + private boolean tryExplicitUrl() { + if (url == null || url.isBlank()) { + return false; + } + return isEndpointReachable(url); + } + + private boolean tryAnthropicApiKey() { + String key = System.getenv("ANTHROPIC_API_KEY"); + if (key != null && !key.isBlank()) { + apiType = ApiType.anthropic; + apiKey = key; + url = DEFAULT_ANTHROPIC_URL; + if (model == null || "llama3.2".equals(model)) { + model = DEFAULT_ANTHROPIC_MODEL; + } + return true; + } + return false; + } + + private boolean tryVertexAi() { + String region = System.getenv("CLOUD_ML_REGION"); + String project = System.getenv("ANTHROPIC_VERTEX_PROJECT_ID"); + if (region != null && !region.isBlank() && project != null && !project.isBlank()) { + apiType = ApiType.anthropic; + vertexRegion = region; + vertexProjectId = project; + if (model == null || "llama3.2".equals(model)) { + model = DEFAULT_ANTHROPIC_MODEL; + } + return true; + } + return false; + } + + private boolean tryAnthropicOrVertex() { + if (url != null && !url.isBlank()) { + return isEndpointReachable(url); + } + return tryAnthropicApiKey() || tryVertexAi(); + } + + private boolean tryOpenAi() { + String key = System.getenv("OPENAI_API_KEY"); + if (key == null || key.isBlank()) { + key = System.getenv("LLM_API_KEY"); + } + if (key != null && !key.isBlank()) { + apiType = ApiType.openai; + apiKey = key; + if (url == null || url.isBlank()) { + url = "https://api.openai.com"; + } + return true; + } + return false; + } + + private boolean tryInfraOllama() { + try { + Map pids = findOllamaPids(); + for (Path pidFile : pids.values()) { + String baseUrl = readBaseUrlFromPidFile(pidFile); + if (baseUrl != null && isEndpointReachable(baseUrl)) { + apiType = ApiType.ollama; + url = baseUrl; + return true; + } + } + } catch (Exception e) { + // ignore + } + return false; + } + + private boolean tryDefaultOllama() { + if (isEndpointReachable(DEFAULT_OLLAMA_URL)) { + apiType = ApiType.ollama; + url = DEFAULT_OLLAMA_URL; + return true; + } + return false; + } + + private void resolveOllamaModel() { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url + "/api/tags")) + .timeout(Duration.ofSeconds(HEALTH_CHECK_TIMEOUT_SECONDS)) + .GET() + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + return; + } + JsonObject json = (JsonObject) Jsoner.deserialize(response.body()); + JsonArray models = (JsonArray) json.get("models"); + if (models == null || models.isEmpty()) { + return; + } + + List available = new ArrayList<>(); + for (Object obj : models) { + JsonObject m = (JsonObject) obj; + String name = m.getString("name"); + if (name != null) { + available.add(name); + } + } + + List preferred + = List.of("qwen3.5", "qwen3", "nemotron-3-nano", "mistral-nemo", + "qwen2.5", "granite4.1", "llama3.1", "llama3.3", "mistral"); + for (String pref : preferred) { + for (String avail : available) { + if (avail.equals(pref) || avail.startsWith(pref + ":")) { + model = avail; + printer.println("Auto-selected model: " + model + " (better tool-calling support)"); + return; + } + } + } + } catch (Exception e) { + // best-effort, keep default + } + } + + boolean isEndpointReachable(String endpoint) { + return tryHealthCheck(endpoint + "/api/tags") + || tryHealthCheck(endpoint + "/v1/models") + || tryHealthCheck(endpoint); + } + + private boolean tryHealthCheck(String healthUrl) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(healthUrl)) + .timeout(Duration.ofSeconds(HEALTH_CHECK_TIMEOUT_SECONDS)) + .GET() + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + return response.statusCode() == 200; + } catch (Exception e) { + return false; + } + } + + private Map findOllamaPids() throws Exception { + Map pids = new HashMap<>(); + Path camelDir = CommandLineHelper.getCamelDir(); + if (!Files.exists(camelDir)) { + return pids; + } + try (Stream fileStream = Files.list(camelDir)) { + fileStream + .filter(p -> { + String name = p.getFileName().toString(); + return name.startsWith("infra-ollama-") && name.endsWith(".json"); + }) + .forEach(p -> { + String name = p.getFileName().toString(); + String pidStr = name.substring(name.lastIndexOf("-") + 1, name.lastIndexOf('.')); + try { + pids.put(Long.valueOf(pidStr), p); + } catch (NumberFormatException e) { + // ignore + } + }); + } + return pids; + } + + private String readBaseUrlFromPidFile(Path pidFile) throws Exception { + String json = Files.readString(pidFile); + JsonObject jo = (JsonObject) Jsoner.deserialize(json); + return jo.getString("baseUrl"); + } + + // ---- URL helpers ---- + + String normalizeOpenAiUrl(String endpoint) { + String u = endpoint.endsWith("/") ? endpoint.substring(0, endpoint.length() - 1) : endpoint; + if (!u.endsWith("/v1/chat/completions")) { + u = u.endsWith("/v1") ? u : u + "/v1"; + u = u + "/chat/completions"; + } + return u; + } + + // ---- Error handling ---- + + private void handleErrorStatus(int statusCode, String body) { + printer.println("LLM returned status: " + statusCode); + switch (statusCode) { + case 401 -> printer.println("Authentication failed. Check your API key."); + case 429 -> printer.println("Rate limit exceeded."); + default -> { + } + } + if (body != null && !body.isBlank()) { + printer.println(body); + } + } + + // ---- OpenAI message helpers ---- + + static JsonObject createOpenAiMessage(String role, String content) { + JsonObject msg = new JsonObject(); + msg.put("role", role); + msg.put("content", content); + return msg; + } + + private static String truncateVerbose(String s) { + if (s == null) { + return "null"; + } + return s.length() > 500 ? s.substring(0, 500) + "... (" + s.length() + " chars)" : s; + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java index da04270700771..7f406b61a6b9e 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java @@ -99,8 +99,11 @@ public Integer doCall() throws Exception { } try (org.jline.shell.Shell shell = builder.build()) { + EnvironmentHelper.setActiveTerminal(shell.terminal()); printBanner(shell, camelVersion, colorEnabled); shell.run(); + } finally { + EnvironmentHelper.setActiveTerminal(null); } return 0; } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java index 4e1395de3fec2..7aed979d3784d 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java @@ -16,7 +16,6 @@ */ package org.apache.camel.dsl.jbang.core.commands.infra; -import java.io.Console; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; @@ -32,6 +31,7 @@ import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.Printer; import org.apache.camel.dsl.jbang.core.common.RuntimeUtil; import org.apache.camel.main.download.DependencyDownloaderClassLoader; @@ -226,15 +226,14 @@ protected Integer doRun(String testService, String testServiceImplementation, Te final CountDownLatch latch = new CountDownLatch(1); // running in foreground then wait for user to exit - final Console c = System.console(); - if (c != null) { + if (EnvironmentHelper.isInteractiveTerminal()) { if (!jsonOutput) { printer().println("Press ENTER to stop the execution"); } Thread t = new Thread(() -> { boolean quit = false; do { - String line = c.readLine(); + String line = EnvironmentHelper.readLine(); if (line != null) { quit = true; latch.countDown(); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java index e35385be27c21..034c59ba1914d 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java @@ -16,6 +16,13 @@ */ package org.apache.camel.dsl.jbang.core.common; +import java.io.BufferedReader; +import java.io.Console; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.jline.terminal.Terminal; + /** * Helper for detecting environment characteristics such as CI environments, color support, and interactive terminals. * @@ -32,9 +39,45 @@ */ public final class EnvironmentHelper { + private static volatile Terminal activeTerminal; + private EnvironmentHelper() { } + /** + * Sets the active JLine terminal. Called by the shell command to make the terminal available to subcommands. + */ + public static void setActiveTerminal(Terminal terminal) { + activeTerminal = terminal; + } + + /** + * Returns the active JLine terminal, or null if not running inside the shell. + */ + public static Terminal getActiveTerminal() { + return activeTerminal; + } + + /** + * Reads a single line from the best available input source: the active JLine terminal if inside the shell, + * otherwise {@link System#console()}. + * + * @return the line read, or null if no input source is available or an error occurs + */ + public static String readLine() { + Terminal terminal = activeTerminal; + if (terminal != null) { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(terminal.input())); + return reader.readLine(); + } catch (IOException e) { + return null; + } + } + Console c = System.console(); + return c != null ? c.readLine() : null; + } + /** * Determines whether colored output should be enabled based on environment variables and terminal capabilities. * @@ -59,7 +102,7 @@ public static boolean isColorEnabled() { if (getEnv("FORCE_COLOR") != null) { return true; } - return System.console() != null; + return activeTerminal != null || System.console() != null; } /** @@ -80,7 +123,7 @@ public static boolean isCIEnvironment() { * @return true if the terminal supports interactive prompts */ public static boolean isInteractiveTerminal() { - return System.console() != null && !isCIEnvironment(); + return (activeTerminal != null || System.console() != null) && !isCIEnvironment(); } // Visible for testing - allows overriding in tests diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java index 02c8fc78f7afc..638cf587db0a5 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java @@ -31,18 +31,7 @@ import org.apache.camel.util.json.JsonObject; /** - * MCP Tools for browsing Camel JBang examples from the - * camel-jbang-examples repository. - *

- * Provides two tools: - *

    - *
  • {@code camel_catalog_examples} — list and filter available examples by name, tag, or difficulty level
  • - *
  • {@code camel_catalog_example_file} — read the content of a specific file from a bundled example, or get a GitHub - * URL for non-bundled ones
  • - *
- * Uses {@link ExampleHelper} for catalog loading, filtering, and file retrieval. - * - * @since 4.21 + * MCP Tools for browsing Camel JBang examples. */ @ApplicationScoped public class ExampleTools { diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java index 0ecdb4f8ce5bf..f51a9cca1c55d 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java @@ -91,13 +91,15 @@ public RouteDiagramResult camel_render_route_diagram( // headless rendering — required when running inside the MCP server System.setProperty("java.awt.headless", "true"); + CapturingPrinter capturingPrinter = new CapturingPrinter(); CamelJBangMain main = new CamelJBangMain() - .withPrinter(new Printer.QuietPrinter(new Printer.SystemOutPrinter())); + .withPrinter(capturingPrinter); CamelRouteDiagramAction action = new CamelRouteDiagramAction(main); try { int exit = action.renderSourceToFile( - sourceFile, resolvedOutput, theme, filter, + sourceFile, resolvedOutput, theme, + filter != null && !filter.isBlank() ? filter : null, width != null ? width : 0, ignoreLoadingError != null && ignoreLoadingError, fontSize != null ? fontSize : 12, @@ -112,13 +114,13 @@ public RouteDiagramResult camel_render_route_diagram( String asciiContent = success ? Files.readString(out.toPath()) : null; String message = success ? "ASCII diagram generated (" + size + " bytes)" - : "Failed to render diagram (exit code " + exit + ")"; + : "Failed to render diagram (exit code " + exit + "): " + capturingPrinter.getOutput(); return new RouteDiagramResult(success, resolvedOutput, size, message, asciiContent); } String message = success ? "Diagram saved to: " + resolvedOutput - : "Failed to render diagram (exit code " + exit + ")"; + : "Failed to render diagram (exit code " + exit + "): " + capturingPrinter.getOutput(); return new RouteDiagramResult(success, resolvedOutput, size, message, null); } catch (Throwable e) { throw new ToolCallException( @@ -132,4 +134,32 @@ public RouteDiagramResult camel_render_route_diagram( public record RouteDiagramResult(boolean success, String outputFile, long sizeBytes, String message, String asciiDiagram) { } + + static class CapturingPrinter implements Printer { + private final StringBuilder sb = new StringBuilder(); + + @Override + public void println() { + sb.append('\n'); + } + + @Override + public void println(String line) { + sb.append(line).append('\n'); + } + + @Override + public void print(String output) { + sb.append(output); + } + + @Override + public void printf(String format, Object... args) { + sb.append(String.format(format, args)); + } + + String getOutput() { + return sb.toString().strip(); + } + } } diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java new file mode 100644 index 0000000000000..7369845a9fa42 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java @@ -0,0 +1,302 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.mcp; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.quarkiverse.mcp.server.Tool; +import io.quarkiverse.mcp.server.ToolArg; +import io.quarkiverse.mcp.server.ToolCallException; +import org.apache.camel.util.json.JsonObject; + +/** + * MCP tools for inspecting and interacting with running Camel applications. + *

+ * These tools communicate with running Camel processes via the file-based IPC protocol in ~/.camel/. Status data is + * read from periodic snapshots; interactive commands use the multi-client action file protocol. + */ +@ApplicationScoped +public class RuntimeTools { + + private static final String NAME_OR_PID_DESC + = "Name or PID of the Camel process. Leave empty to auto-detect (works when exactly one Camel process is running)"; + + @Inject + RuntimeService runtimeService; + + // ---- Process discovery ---- + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = """ + List all running Camel processes that can be inspected. \ + Returns PID, name, and context name for each discovered process.""") + public List camel_runtime_processes() { + return runtimeService.discoverProcesses(); + } + + // ---- Read-only tools (from status file) ---- + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "Get Camel context information: name, version, state, uptime, route count, exchange statistics.") + public JsonObject camel_runtime_context( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.readStatusSection(p.pid(), "context"); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "List Camel routes with their state, uptime, messages processed, last error, and throughput statistics.") + public JsonObject camel_runtime_routes( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.readStatusSection(p.pid(), "routes"); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "Get health check status for the Camel application.") + public JsonObject camel_runtime_health( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.readStatusSection(p.pid(), "healthChecks"); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "List all endpoints registered in the Camel context with their URIs and usage statistics.") + public JsonObject camel_runtime_endpoints( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.readStatusSection(p.pid(), "endpoints"); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "Show currently in-flight exchanges (messages being processed).") + public JsonObject camel_runtime_inflight( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.readStatusSection(p.pid(), "inflight"); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "Show blocked exchanges that are stuck or waiting.") + public JsonObject camel_runtime_blocked( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.readStatusSection(p.pid(), "blocked"); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "Show exchange variables in the Camel context.") + public JsonObject camel_runtime_variables( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.readStatusSection(p.pid(), "variables"); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "Show consumer statistics (polling consumers, event-driven consumers).") + public JsonObject camel_runtime_consumers( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.readStatusSection(p.pid(), "consumers"); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "Show configuration properties of the running Camel application.") + public JsonObject camel_runtime_properties( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.readStatusSection(p.pid(), "properties"); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "Show services registered in the Camel service registry.") + public JsonObject camel_runtime_services( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.readStatusSection(p.pid(), "services"); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "Show JVM memory usage (heap/non-heap), garbage collection stats, and thread counts.") + public JsonObject camel_runtime_memory( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + JsonObject status = runtimeService.readStatus(p.pid()); + if (status == null) { + return new JsonObject(); + } + JsonObject result = new JsonObject(); + if (status.containsKey("memory")) { + result.put("memory", status.get("memory")); + } + if (status.containsKey("gc")) { + result.put("gc", status.get("gc")); + } + if (status.containsKey("threads")) { + result.put("threads", status.get("threads")); + } + return result; + } + + // ---- Interactive tools (via action file IPC) ---- + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "Get the source code of routes in the running Camel application.") + public JsonObject camel_runtime_route_source( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid, + @ToolArg(description = "Filter source files by name (supports wildcards)") String filter) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.executeAction(p.pid(), "source", root -> { + root.put("filter", filter != null ? filter : "*"); + }); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "Dump route definitions in XML or YAML format.") + public JsonObject camel_runtime_route_dump( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid, + @ToolArg(description = "Route ID to dump (use * for all routes)") String routeId, + @ToolArg(description = "Output format: xml or yaml (default: yaml)") String format) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.executeAction(p.pid(), "route-dump", root -> { + root.put("id", routeId != null ? routeId : "*"); + root.put("format", format != null ? format : "yaml"); + }); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "Show the route structure as a tree of processors.") + public JsonObject camel_runtime_route_structure( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid, + @ToolArg(description = "Route ID to inspect (use * for all routes)") String routeId) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.executeAction(p.pid(), "route-structure", root -> { + root.put("id", routeId != null ? routeId : "*"); + }); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = false, destructiveHint = true, openWorldHint = false), + description = "Control a route: start, stop, suspend, or resume it.") + public JsonObject camel_runtime_route_control( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid, + @ToolArg(description = "Route ID to control") String routeId, + @ToolArg(description = "Command: start, stop, suspend, or resume") String command) { + if (routeId == null || routeId.isBlank()) { + throw new ToolCallException("routeId is required", null); + } + if (command == null || command.isBlank()) { + throw new ToolCallException("command is required (start, stop, suspend, resume)", null); + } + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.executeAction(p.pid(), "route", root -> { + root.put("id", routeId); + root.put("command", command); + }); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = false, destructiveHint = true, openWorldHint = false), + description = "Send a test message to a Camel endpoint in the running application.") + public JsonObject camel_runtime_send( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid, + @ToolArg(description = "Endpoint URI to send to (e.g., direct:myRoute, seda:queue)") String endpoint, + @ToolArg(description = "Message body to send") String body, + @ToolArg(description = "Message headers as key=value pairs separated by newlines") String headers) { + if (endpoint == null || endpoint.isBlank()) { + throw new ToolCallException("endpoint is required", null); + } + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.executeAction(p.pid(), "send", root -> { + root.put("endpoint", endpoint); + if (body != null) { + root.put("body", body); + } + if (headers != null) { + root.put("headers", headers); + } + }); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = false, destructiveHint = false, openWorldHint = false), + description = "Enable, disable, or dump message tracing for the running Camel application.") + public JsonObject camel_runtime_trace( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid, + @ToolArg(description = "Action: enable, disable, or dump") String action) { + if (action == null || action.isBlank()) { + throw new ToolCallException("action is required (enable, disable, dump)", null); + } + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.executeAction(p.pid(), "trace", root -> { + switch (action.toLowerCase()) { + case "enable" -> root.put("enabled", "true"); + case "disable" -> root.put("enabled", "false"); + case "dump" -> root.put("dump", "true"); + default -> throw new ToolCallException( + "Unknown trace action: " + action + + ". Use 'enable', 'disable', or 'dump'.", + null); + } + }); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "Show top processor statistics: which processors are slowest and most active.") + public JsonObject camel_runtime_top( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.executeAction(p.pid(), "top-processors", null); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = """ + Evaluate an expression in the given language (e.g., simple, jsonpath, xpath) \ + against the Camel context.""") + public JsonObject camel_runtime_eval( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid, + @ToolArg(description = "Expression language (e.g., simple, jsonpath, xpath, jq)") String language, + @ToolArg(description = "Expression to evaluate") String expression) { + if (language == null || language.isBlank()) { + throw new ToolCallException("language is required", null); + } + if (expression == null || expression.isBlank()) { + throw new ToolCallException("expression is required", null); + } + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.executeAction(p.pid(), "eval", root -> { + root.put("language", language); + root.put("expression", expression); + }); + } + + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = "Browse messages in a Camel endpoint (e.g., browse messages queued in a SEDA endpoint).") + public JsonObject camel_runtime_browse( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid, + @ToolArg(description = "Endpoint URI to browse") String endpoint, + @ToolArg(description = "Maximum number of messages to return (default: 50)") Integer limit) { + if (endpoint == null || endpoint.isBlank()) { + throw new ToolCallException("endpoint is required", null); + } + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.executeAction(p.pid(), "browse", root -> { + root.put("endpoint", endpoint); + root.put("limit", limit != null ? limit : 50); + }); + } +} diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeServiceTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeServiceTest.java new file mode 100644 index 0000000000000..3581934ede234 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeServiceTest.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.mcp; + +import java.util.List; + +import io.quarkiverse.mcp.server.ToolCallException; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RuntimeServiceTest { + + @Test + void discoverProcessesReturnsEmptyWhenNoneRunning() { + RuntimeService service = new RuntimeService(); + List processes = service.discoverProcesses(); + // may or may not find processes depending on environment, + // but should not throw + assertThat(processes).isNotNull(); + } + + @Test + void findSingleProcessThrowsWhenNoneRunning() { + RuntimeService service = new RuntimeService(); + List processes = service.discoverProcesses(); + if (processes.isEmpty()) { + assertThatThrownBy(() -> service.findSingleProcess(null)) + .isInstanceOf(ToolCallException.class) + .hasMessageContaining("No running Camel processes found"); + } + } + + @Test + void findSingleProcessThrowsForInvalidPid() { + RuntimeService service = new RuntimeService(); + assertThatThrownBy(() -> service.findSingleProcess("99999999")) + .isInstanceOf(ToolCallException.class) + .hasMessageContaining("Camel process"); + } + + @Test + void readStatusReturnsNullForNonExistentPid() { + RuntimeService service = new RuntimeService(); + assertThat(service.readStatus(99999999L)).isNull(); + } + + @Test + void readStatusSectionThrowsForNonExistentPid() { + RuntimeService service = new RuntimeService(); + assertThatThrownBy(() -> service.readStatusSection(99999999L, "context")) + .isInstanceOf(ToolCallException.class) + .hasMessageContaining("No status available"); + } +} diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsTest.java new file mode 100644 index 0000000000000..8bdd1dafb215b --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsTest.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.mcp; + +import java.util.List; + +import io.quarkiverse.mcp.server.ToolCallException; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RuntimeToolsTest { + + private RuntimeTools createTools() { + RuntimeTools tools = new RuntimeTools(); + tools.runtimeService = new RuntimeService(); + return tools; + } + + @Test + void processesReturnsListWithoutThrowing() { + RuntimeTools tools = createTools(); + List result = tools.camel_runtime_processes(); + assertThat(result).isNotNull(); + } + + @Test + void contextThrowsWhenNoProcessRunning() { + RuntimeTools tools = createTools(); + List processes = tools.camel_runtime_processes(); + if (processes.isEmpty()) { + assertThatThrownBy(() -> tools.camel_runtime_context(null)) + .isInstanceOf(ToolCallException.class) + .hasMessageContaining("No running Camel processes"); + } + } + + @Test + void routeControlRequiresRouteId() { + RuntimeTools tools = createTools(); + assertThatThrownBy(() -> tools.camel_runtime_route_control(null, null, "start")) + .isInstanceOf(ToolCallException.class) + .hasMessageContaining("routeId is required"); + } + + @Test + void routeControlRequiresCommand() { + RuntimeTools tools = createTools(); + assertThatThrownBy(() -> tools.camel_runtime_route_control(null, "myRoute", null)) + .isInstanceOf(ToolCallException.class) + .hasMessageContaining("command is required"); + } + + @Test + void sendRequiresEndpoint() { + RuntimeTools tools = createTools(); + assertThatThrownBy(() -> tools.camel_runtime_send(null, null, "body", null)) + .isInstanceOf(ToolCallException.class) + .hasMessageContaining("endpoint is required"); + } + + @Test + void evalRequiresLanguage() { + RuntimeTools tools = createTools(); + assertThatThrownBy(() -> tools.camel_runtime_eval(null, null, "expr")) + .isInstanceOf(ToolCallException.class) + .hasMessageContaining("language is required"); + } + + @Test + void evalRequiresExpression() { + RuntimeTools tools = createTools(); + assertThatThrownBy(() -> tools.camel_runtime_eval(null, "simple", null)) + .isInstanceOf(ToolCallException.class) + .hasMessageContaining("expression is required"); + } + + @Test + void traceRequiresAction() { + RuntimeTools tools = createTools(); + assertThatThrownBy(() -> tools.camel_runtime_trace(null, null)) + .isInstanceOf(ToolCallException.class) + .hasMessageContaining("action is required"); + } + + @Test + void browseRequiresEndpoint() { + RuntimeTools tools = createTools(); + assertThatThrownBy(() -> tools.camel_runtime_browse(null, null, null)) + .isInstanceOf(ToolCallException.class) + .hasMessageContaining("endpoint is required"); + } +}