davsclaus commented on code in PR #23600:
URL: https://github.com/apache/camel/pull/23600#discussion_r3319997761


##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java:
##########
@@ -720,6 +749,213 @@ private String executeGetExampleFile(JsonObject args) {
         }
     }
 
+    // ---- CLI tools ----
+
+    @SuppressWarnings("unchecked")
+    private List<JsonObject> loadCommandMetadata() {
+        try (InputStream is = getClass().getClassLoader()
+                
.getResourceAsStream("META-INF/camel-jbang-commands-metadata.json")) {
+            if (is == null) {
+                return List.of();
+            }
+            String json = IOHelper.loadText(is);
+            JsonObject root = (JsonObject) Jsoner.deserialize(json);

Review Comment:
   Minor: `loadCommandMetadata()` is called fresh on each tool invocation. 
Since the metadata JSON is a static classpath resource, consider caching the 
parsed result in a field (e.g., `private List<JsonObject> cachedCommands;`) to 
avoid repeated I/O and parsing during a chat session.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java:
##########
@@ -720,6 +749,213 @@ private String executeGetExampleFile(JsonObject args) {
         }
     }
 
+    // ---- CLI tools ----
+
+    @SuppressWarnings("unchecked")
+    private List<JsonObject> loadCommandMetadata() {
+        try (InputStream is = getClass().getClassLoader()
+                
.getResourceAsStream("META-INF/camel-jbang-commands-metadata.json")) {
+            if (is == null) {
+                return List.of();
+            }
+            String json = IOHelper.loadText(is);
+            JsonObject root = (JsonObject) Jsoner.deserialize(json);
+            Object commands = root.get("commands");
+            if (commands instanceof Collection<?>) {
+                return ((Collection<Object>) commands).stream()
+                        .filter(JsonObject.class::isInstance)
+                        .map(JsonObject.class::cast)
+                        .toList();
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        return List.of();
+    }
+
+    @SuppressWarnings("unchecked")
+    private void collectCommands(List<JsonObject> commands, List<JsonObject> 
result, String filter) {
+        for (JsonObject cmd : commands) {
+            String fullName = cmd.getString("fullName");
+            String description = cmd.getString("description");
+            boolean matches = filter == null || filter.isBlank()
+                    || (fullName != null && 
fullName.toLowerCase().contains(filter.toLowerCase()))
+                    || (description != null && 
description.toLowerCase().contains(filter.toLowerCase()));
+            if (matches) {
+                JsonObject entry = new JsonObject();
+                entry.put("command", fullName);
+                entry.put("description", description);
+                Object subs = cmd.get("subcommands");
+                if (subs instanceof Collection<?> subList && 
!subList.isEmpty()) {
+                    entry.put("hasSubcommands", true);
+                    entry.put("subcommandCount", subList.size());
+                }
+                result.add(entry);
+            }
+            Object subs = cmd.get("subcommands");
+            if (subs instanceof Collection<?>) {
+                collectCommands(
+                        ((Collection<Object>) subs).stream()
+                                .filter(JsonObject.class::isInstance)
+                                .map(JsonObject.class::cast)
+                                .toList(),
+                        result, filter);
+            }
+        }
+    }
+
+    private String executeCliListCommands(JsonObject args) {
+        String filter = args.getString("filter");
+        List<JsonObject> commands = loadCommandMetadata();
+        List<JsonObject> result = new ArrayList<>();
+        collectCommands(commands, result, filter);
+
+        JsonObject response = new JsonObject();
+        response.put("count", result.size());
+        response.put("commands", result);
+        return response.toJson();
+    }
+
+    @SuppressWarnings("unchecked")
+    private JsonObject findCommand(List<JsonObject> commands, String fullName) 
{
+        for (JsonObject cmd : commands) {
+            if (fullName.equals(cmd.getString("fullName"))) {
+                return cmd;
+            }
+            Object subs = cmd.get("subcommands");
+            if (subs instanceof Collection<?>) {
+                JsonObject found = findCommand(
+                        ((Collection<Object>) subs).stream()
+                                .filter(JsonObject.class::isInstance)
+                                .map(JsonObject.class::cast)
+                                .toList(),
+                        fullName);
+                if (found != null) {
+                    return found;
+                }
+            }
+        }
+        return null;
+    }
+
+    @SuppressWarnings("unchecked")
+    private String executeCliCommandHelp(JsonObject args) {
+        String command = args.getString("command");
+        if (command == null || command.isBlank()) {
+            return "Error: command name is required";
+        }
+
+        List<JsonObject> commands = loadCommandMetadata();
+        JsonObject cmd = findCommand(commands, command);
+        if (cmd == null) {
+            return "Command not found: " + command + ". Use cli_list_commands 
to see available commands.";
+        }
+
+        JsonObject response = new JsonObject();
+        response.put("command", cmd.getString("fullName"));
+        response.put("description", cmd.getString("description"));
+
+        Object options = cmd.get("options");
+        if (options instanceof Collection<?>) {
+            List<JsonObject> opts = ((Collection<Object>) options).stream()
+                    .filter(JsonObject.class::isInstance)
+                    .map(JsonObject.class::cast)
+                    .map(opt -> {
+                        JsonObject o = new JsonObject();
+                        o.put("names", opt.getString("names"));
+                        o.put("description", opt.getString("description"));
+                        o.put("type", opt.getString("type"));
+                        String dv = opt.getString("defaultValue");
+                        if (dv != null) {
+                            o.put("defaultValue", dv);
+                        }
+                        return o;
+                    })
+                    .toList();
+            response.put("options", opts);
+        }
+
+        Object subs = cmd.get("subcommands");
+        if (subs instanceof Collection<?> subList && !subList.isEmpty()) {
+            List<JsonObject> subSummaries = ((Collection<Object>) 
subList).stream()
+                    .filter(JsonObject.class::isInstance)
+                    .map(JsonObject.class::cast)
+                    .map(sub -> {
+                        JsonObject s = new JsonObject();
+                        s.put("command", sub.getString("fullName"));
+                        s.put("description", sub.getString("description"));
+                        return s;
+                    })
+                    .toList();
+            response.put("subcommands", subSummaries);
+        }
+
+        return response.toJson();
+    }
+
+    private String executeCliExec(JsonObject args) {
+        String command = args.getString("command");
+        if (command == null || command.isBlank()) {
+            return "Error: command is required";
+        }
+
+        picocli.CommandLine commandLine = CamelJBangMain.getCommandLine();
+        if (commandLine == null) {
+            return "Error: CLI not available";
+        }
+
+        String[] cmdArgs = command.trim().split("\\s+");
+
+        // capture output by temporarily swapping the Printer on main
+        StringBuilder captured = new StringBuilder();
+        Printer capturingPrinter = new Printer() {

Review Comment:
   Question: `command.trim().split("\\s+")` won't handle quoted arguments 
correctly (e.g., `get route --filter="my route"`). Is this a known limitation? 
If so, it might be worth noting in the tool description so the LLM avoids 
passing values with spaces.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java:
##########
@@ -720,6 +749,213 @@ private String executeGetExampleFile(JsonObject args) {
         }
     }
 
+    // ---- CLI tools ----
+
+    @SuppressWarnings("unchecked")
+    private List<JsonObject> loadCommandMetadata() {
+        try (InputStream is = getClass().getClassLoader()
+                
.getResourceAsStream("META-INF/camel-jbang-commands-metadata.json")) {
+            if (is == null) {
+                return List.of();
+            }
+            String json = IOHelper.loadText(is);
+            JsonObject root = (JsonObject) Jsoner.deserialize(json);
+            Object commands = root.get("commands");
+            if (commands instanceof Collection<?>) {
+                return ((Collection<Object>) commands).stream()
+                        .filter(JsonObject.class::isInstance)
+                        .map(JsonObject.class::cast)
+                        .toList();
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        return List.of();
+    }
+
+    @SuppressWarnings("unchecked")
+    private void collectCommands(List<JsonObject> commands, List<JsonObject> 
result, String filter) {
+        for (JsonObject cmd : commands) {
+            String fullName = cmd.getString("fullName");
+            String description = cmd.getString("description");
+            boolean matches = filter == null || filter.isBlank()
+                    || (fullName != null && 
fullName.toLowerCase().contains(filter.toLowerCase()))
+                    || (description != null && 
description.toLowerCase().contains(filter.toLowerCase()));
+            if (matches) {
+                JsonObject entry = new JsonObject();
+                entry.put("command", fullName);
+                entry.put("description", description);
+                Object subs = cmd.get("subcommands");
+                if (subs instanceof Collection<?> subList && 
!subList.isEmpty()) {
+                    entry.put("hasSubcommands", true);
+                    entry.put("subcommandCount", subList.size());
+                }
+                result.add(entry);
+            }
+            Object subs = cmd.get("subcommands");
+            if (subs instanceof Collection<?>) {
+                collectCommands(
+                        ((Collection<Object>) subs).stream()
+                                .filter(JsonObject.class::isInstance)
+                                .map(JsonObject.class::cast)
+                                .toList(),
+                        result, filter);
+            }
+        }
+    }
+
+    private String executeCliListCommands(JsonObject args) {
+        String filter = args.getString("filter");
+        List<JsonObject> commands = loadCommandMetadata();
+        List<JsonObject> result = new ArrayList<>();
+        collectCommands(commands, result, filter);
+
+        JsonObject response = new JsonObject();
+        response.put("count", result.size());
+        response.put("commands", result);
+        return response.toJson();
+    }
+
+    @SuppressWarnings("unchecked")
+    private JsonObject findCommand(List<JsonObject> commands, String fullName) 
{
+        for (JsonObject cmd : commands) {
+            if (fullName.equals(cmd.getString("fullName"))) {
+                return cmd;
+            }
+            Object subs = cmd.get("subcommands");
+            if (subs instanceof Collection<?>) {
+                JsonObject found = findCommand(
+                        ((Collection<Object>) subs).stream()
+                                .filter(JsonObject.class::isInstance)
+                                .map(JsonObject.class::cast)
+                                .toList(),
+                        fullName);
+                if (found != null) {
+                    return found;
+                }
+            }
+        }
+        return null;
+    }
+
+    @SuppressWarnings("unchecked")
+    private String executeCliCommandHelp(JsonObject args) {
+        String command = args.getString("command");
+        if (command == null || command.isBlank()) {
+            return "Error: command name is required";
+        }
+
+        List<JsonObject> commands = loadCommandMetadata();
+        JsonObject cmd = findCommand(commands, command);
+        if (cmd == null) {
+            return "Command not found: " + command + ". Use cli_list_commands 
to see available commands.";
+        }
+
+        JsonObject response = new JsonObject();
+        response.put("command", cmd.getString("fullName"));
+        response.put("description", cmd.getString("description"));
+
+        Object options = cmd.get("options");
+        if (options instanceof Collection<?>) {
+            List<JsonObject> opts = ((Collection<Object>) options).stream()
+                    .filter(JsonObject.class::isInstance)
+                    .map(JsonObject.class::cast)
+                    .map(opt -> {
+                        JsonObject o = new JsonObject();
+                        o.put("names", opt.getString("names"));
+                        o.put("description", opt.getString("description"));
+                        o.put("type", opt.getString("type"));
+                        String dv = opt.getString("defaultValue");
+                        if (dv != null) {
+                            o.put("defaultValue", dv);
+                        }
+                        return o;
+                    })
+                    .toList();
+            response.put("options", opts);
+        }
+
+        Object subs = cmd.get("subcommands");
+        if (subs instanceof Collection<?> subList && !subList.isEmpty()) {
+            List<JsonObject> subSummaries = ((Collection<Object>) 
subList).stream()
+                    .filter(JsonObject.class::isInstance)
+                    .map(JsonObject.class::cast)
+                    .map(sub -> {
+                        JsonObject s = new JsonObject();
+                        s.put("command", sub.getString("fullName"));
+                        s.put("description", sub.getString("description"));
+                        return s;
+                    })
+                    .toList();
+            response.put("subcommands", subSummaries);
+        }
+
+        return response.toJson();
+    }
+
+    private String executeCliExec(JsonObject args) {
+        String command = args.getString("command");
+        if (command == null || command.isBlank()) {
+            return "Error: command is required";
+        }
+
+        picocli.CommandLine commandLine = CamelJBangMain.getCommandLine();
+        if (commandLine == null) {
+            return "Error: CLI not available";
+        }
+
+        String[] cmdArgs = command.trim().split("\\s+");
+
+        // capture output by temporarily swapping the Printer on main
+        StringBuilder captured = new StringBuilder();
+        Printer capturingPrinter = new Printer() {
+            @Override
+            public void println() {
+                captured.append(System.lineSeparator());
+            }
+
+            @Override
+            public void println(String line) {
+                captured.append(line).append(System.lineSeparator());
+            }
+
+            @Override
+            public void print(String output) {
+                captured.append(output);
+            }
+
+            @Override
+            public void printf(String format, Object... fmtArgs) {
+                captured.append(String.format(format, fmtArgs));
+            }
+        };
+
+        // also capture PicoCLI's own output (usage/help text)
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+        commandLine.setOut(pw);
+        commandLine.setErr(pw);
+
+        Printer originalPrinter = getMain().getOut();
+        getMain().setOut(capturingPrinter);
+        try {
+            int exitCode = commandLine.execute(cmdArgs);
+            pw.flush();
+            String output = captured.toString() + sw.toString();
+            if (output.isBlank() && exitCode != 0) {
+                return "Command exited with code " + exitCode;
+            }
+            if (output.length() > 32768) {
+                output = output.substring(0, 32768) + "\n... (truncated)";
+            }
+            return output;
+        } catch (Exception e) {
+            return "Error executing command: " + e.getMessage();

Review Comment:
   Minor: The `finally` block restores `getMain().setOut(originalPrinter)` but 
doesn't restore picocli's `commandLine.setOut()` / `setErr()` to their original 
values. If an exception occurs between setting them (lines 934-935) and the 
`finally`, the writers stay swapped for subsequent calls.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to