This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 0be59c361c05 CAMEL-24828: the examples tools of camel-jbang-mcp list 
the examples by group
0be59c361c05 is described below

commit 0be59c361c051d5365501eb63c7b7588a739fd92
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Sep 19 07:11:58 2026 +0200

    CAMEL-24828: the examples tools of camel-jbang-mcp list the examples by 
group
    
    The examples tools lagged behind the reorganized examples catalog
    (CAMEL-24824): camel_catalog_examples in camel-jbang-mcp, list_examples in
    the camel-jbang-core AI tool registry and tui_list_examples in the TUI
    plugin. In camel-jbang-mcp the arguments were not declared optional, so a
    bare call failed with "Missing required argument"; the descriptions still
    spoke of beginner/intermediate/advanced while the catalog has the 11 groups;
    order, teaches and infraServices were not returned, the listings came in
    alphabetical order, and the core registry capped the result at 20 of the 40
    examples.
    
    All three listings now take optional filter, level (a group) and limit
    (default 50, capped across all groups), and return the groups of the ladder
    (level, title, intro, count) and the examples in reading order with order,
    teaches, infraServices, bundled and files, so an agent can present the same
    ladder as camel run --example. ExampleHelper.getTeaches is the new map
    accessor behind getTeachesSummary.
    
    Closes #26607
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   | 71 +++++++++++++++------
 .../camel/dsl/jbang/core/common/ExampleHelper.java | 28 ++++++---
 .../jbang/core/commands/ai/ToolRegistryTest.java   | 27 ++++++++
 .../dsl/jbang/core/commands/mcp/ExampleTools.java  | 67 +++++++++++++-------
 .../jbang/core/commands/mcp/ExampleToolsTest.java  | 72 ++++++++++++++++++++++
 .../core/commands/tui/TuiToolDefinitions.java      | 16 +++--
 .../jbang/core/commands/tui/TuiToolRegistry.java   | 64 ++++++++++++-------
 .../commands/tui/TuiToolRegistryLaunchTest.java    | 24 ++++++++
 8 files changed, 289 insertions(+), 80 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
index ca4b617cc884..6dba0839df9b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -942,39 +942,74 @@ public final class ToolRegistry {
 
     private static void registerExampleTools() {
         register(tool("list_examples",
-                "List available Camel CLI examples. Returns name, title, 
description, difficulty level, and tags.")
+                "List the Camel CLI examples, grouped as the ladder of the 
examples (quick-start, run, transform, "
+                                       + "route, fail-well, connect, 
connect-service, contracts, ai, cloud, showcase). "
+                                       + "Returns the groups (level, title, 
intro) and the examples in reading order with "
+                                       + "name, title, description, level, 
order, tags, what they teach, the infra "
+                                       + "services they need, whether they are 
bundled and their files. "
+                                       + "Call it without arguments for the 
whole ladder, with level for one group.")
                 .param("filter", "string",
                         "Filter by name, description, or tag 
(case-insensitive)", false)
                 .param("level", "string",
-                        "Filter by difficulty: beginner, intermediate, or 
advanced", false)
+                        "Only the examples of one group: quick-start, run, 
transform, route, fail-well, connect, "
+                                          + "connect-service, contracts, ai, 
cloud or showcase",
+                        false)
+                .param("limit", "integer", "Maximum number of examples to 
return (default: 50)", false)
                 .executor((ctx, args) -> {
                     String filter = args.get("filter");
                     String level = args.get("level");
+                    int limit = 50;
+                    String limitArg = args.get("limit");
+                    if (limitArg != null && !limitArg.isBlank()) {
+                        try {
+                            limit = Integer.parseInt(limitArg.trim());
+                            if (limit <= 0) {
+                                limit = 50;
+                            }
+                        } catch (NumberFormatException e) {
+                            throw new ToolExecutionException("limit must be a 
number: " + limitArg);
+                        }
+                    }
                     List<JsonObject> catalog2 = ExampleHelper.loadCatalog();
                     List<JsonObject> filtered = 
ExampleHelper.filterExamples(catalog2, filter);
+                    List<JsonObject> groups = new ArrayList<>();
                     List<JsonObject> results = new ArrayList<>();
-                    for (JsonObject entry : filtered) {
-                        if (level != null && !level.isBlank()) {
-                            String entryLevel = entry.getString("level");
-                            if (entryLevel == null || 
!entryLevel.equalsIgnoreCase(level)) {
+                    int total = 0;
+                    for (Map.Entry<String, List<JsonObject>> group : 
ExampleHelper.groupByLevel(filtered).entrySet()) {
+                        if (level != null && !level.isBlank() && 
!group.getKey().equalsIgnoreCase(level)) {
+                            continue;
+                        }
+                        total += group.getValue().size();
+                        JsonObject g = new JsonObject();
+                        g.put("level", group.getKey());
+                        g.put("title", 
ExampleHelper.getGroupTitle(group.getKey()));
+                        g.put("intro", 
ExampleHelper.getGroupIntro(group.getKey()));
+                        g.put("count", group.getValue().size());
+                        groups.add(g);
+                        for (JsonObject entry : group.getValue()) {
+                            if (results.size() >= limit) {
                                 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 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"));
+                            if (ExampleHelper.getOrder(entry) != 
Integer.MAX_VALUE) {
+                                jo.put("order", ExampleHelper.getOrder(entry));
+                            }
+                            jo.put("tags", entry.get("tags"));
+                            jo.put("teaches", ExampleHelper.getTeaches(entry));
+                            jo.put("infraServices", 
ExampleHelper.getInfraServices(entry));
+                            jo.put("bundled", ExampleHelper.isBundled(entry));
+                            jo.put("files", ExampleHelper.getFiles(entry));
+                            results.add(jo);
                         }
                     }
                     JsonObject response = new JsonObject();
                     response.put("count", results.size());
+                    response.put("total", total);
+                    response.put("groups", groups);
                     response.put("examples", results);
                     return response.toJson();
                 }));
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
index bfdc7cb4aafc..febdca5fd094 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
@@ -255,14 +255,15 @@ public final class ExampleHelper {
     }
 
     /**
-     * What the example teaches, as one line: the components and the EIPs from 
its metadata, or an empty string.
+     * What the example teaches from its metadata: the components, EIPs, 
languages and data formats, each as a list of
+     * names in that order; keys without names are left out, so an example 
without metadata gives an empty map.
      */
-    public static String getTeachesSummary(JsonObject entry) {
+    public static Map<String, List<String>> getTeaches(JsonObject entry) {
+        Map<String, List<String>> answer = new LinkedHashMap<>();
         JsonObject teaches = entry.getMap("teaches");
         if (teaches == null || teaches.isEmpty()) {
-            return "";
+            return answer;
         }
-        StringBuilder sb = new StringBuilder();
         for (String key : new String[] { "components", "eips", "languages", 
"dataformats" }) {
             // the catalog holds string arrays here; anything else in a 
hand-edited metadata file is skipped
             if (!(teaches.get(key) instanceof Collection<?> values) || 
values.isEmpty()) {
@@ -275,11 +276,22 @@ public final class ExampleHelper {
                 }
             }
             if (!names.isEmpty()) {
-                if (sb.length() > 0) {
-                    sb.append(" · ");
-                }
-                sb.append(key).append(": ").append(String.join(", ", names));
+                answer.put(key, names);
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * What the example teaches, as one line: the components and the EIPs from 
its metadata, or an empty string.
+     */
+    public static String getTeachesSummary(JsonObject entry) {
+        StringBuilder sb = new StringBuilder();
+        for (Map.Entry<String, List<String>> e : getTeaches(entry).entrySet()) 
{
+            if (sb.length() > 0) {
+                sb.append(" · ");
             }
+            sb.append(e.getKey()).append(": ").append(String.join(", ", 
e.getValue()));
         }
         return sb.toString();
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
index 514e2d77c0a9..f08d209c07d3 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
@@ -88,6 +88,33 @@ class ToolRegistryTest {
         assertTrue(json.contains("split"), "Should find split EIP");
     }
 
+    @Test
+    void listExamplesGroupsTheLadderWithoutArguments() {
+        ToolContext ctx = new ToolContext();
+        String json = ToolRegistry.execute("list_examples", ctx, 
Map.of()).toString();
+        assertTrue(json.contains("\"groups\""), "Should return the groups");
+        assertTrue(json.contains("\"level\":\"quick-start\""), "Should start 
with the quick-start group");
+        assertTrue(json.contains("timer-log"), "Should list the examples");
+        assertTrue(json.contains("\"teaches\""), "Should tell what the 
examples teach");
+        // more than the old cap of 20 examples
+        assertTrue(json.indexOf("\"total\":") > 0);
+        String total = json.replaceAll(".*\"total\":(\\d+).*", "$1");
+        assertTrue(Integer.parseInt(total) > 20, "Should count all examples, 
got " + total);
+    }
+
+    @Test
+    void listExamplesFiltersOneGroupAndLimits() {
+        ToolContext ctx = new ToolContext();
+        String json = ToolRegistry.execute("list_examples", ctx, 
Map.of("level", "run", "limit", "1")).toString();
+        assertTrue(json.contains("\"count\":1"), "Should honour the limit: " + 
json);
+        assertTrue(json.contains("\"level\":\"run\""));
+        assertFalse(json.contains("\"level\":\"quick-start\""), "Should only 
return the run group");
+        assertThrows(ToolExecutionException.class,
+                () -> ToolRegistry.execute("list_examples", ctx, 
Map.of("limit", "many")));
+        String zero = ToolRegistry.execute("list_examples", ctx, 
Map.of("level", "run", "limit", "0")).toString();
+        assertFalse(zero.contains("\"count\":0"), "limit 0 falls back to the 
default: " + zero.substring(0, 60));
+    }
+
     @Test
     void runtimeToolThrowsWithoutProcess() {
         ToolContext ctx = new ToolContext();
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 c2c2d3d5c6e6..7811acb5a0df 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
@@ -20,6 +20,7 @@ import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 import jakarta.enterprise.context.ApplicationScoped;
 
@@ -38,38 +39,51 @@ import org.apache.camel.util.json.JsonObject;
 public class ExampleTools {
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
-          description = "List available Camel CLI examples. "
-                        + "Returns name, title, description, difficulty level, 
and tags. "
-                        + "Use filter to search by name, description, or tag. "
-                        + "Use level to filter by difficulty (beginner, 
intermediate, advanced).")
+          description = "List the Camel CLI examples, grouped as the ladder of 
the examples: "
+                        + "quick-start, run, transform, route, fail-well, 
connect, connect-service, contracts, ai, "
+                        + "cloud and showcase, in that reading order. Returns 
the groups (level, title, introduction) "
+                        + "and the examples in reading order with name, title, 
description, level, order, tags, "
+                        + "what they teach (components, EIPs, languages, data 
formats), the infra services they need "
+                        + "(start them with camel infra run), whether they are 
bundled and their files. "
+                        + "Call it without arguments for the whole ladder, 
with level for one group, "
+                        + "or with filter to search by name, description or 
tag. "
+                        + "Use camel_catalog_example_file to read a file of an 
example.")
     public ExampleListResult camel_catalog_examples(
-            @ToolArg(description = "Filter examples by name, description, or 
tag (case-insensitive substring match)") String filter,
-            @ToolArg(description = "Filter by difficulty level: beginner, 
intermediate, or advanced") String level,
-            @ToolArg(description = "Maximum number of results to return 
(default: 50)") Integer limit) {
+            @ToolArg(description = "Filter examples by name, description, or 
tag (case-insensitive substring match)",
+                     required = false) String filter,
+            @ToolArg(description = "Only the examples of one group (level): 
quick-start, run, transform, route, "
+                                   + "fail-well, connect, connect-service, 
contracts, ai, cloud or showcase",
+                     required = false) String level,
+            @ToolArg(description = "Maximum number of examples to return 
(default: 50)",
+                     required = false) Integer limit) {
 
-        int maxResults = limit != null ? limit : 50;
+        int maxResults = limit != null && limit > 0 ? limit : 50;
 
         try {
             List<JsonObject> catalog = ExampleHelper.loadCatalog();
             List<JsonObject> filtered = ExampleHelper.filterExamples(catalog, 
filter);
 
+            List<GroupInfo> groups = new ArrayList<>();
             List<ExampleInfo> result = new ArrayList<>();
-            for (JsonObject entry : filtered) {
-                if (level != null && !level.isBlank()) {
-                    String entryLevel = entry.getString("level");
-                    if (entryLevel == null || 
!entryLevel.equalsIgnoreCase(level)) {
-                        continue;
-                    }
+            int total = 0;
+            for (Map.Entry<String, List<JsonObject>> group : 
ExampleHelper.groupByLevel(filtered).entrySet()) {
+                if (level != null && !level.isBlank() && 
!group.getKey().equalsIgnoreCase(level)) {
+                    continue;
                 }
-
-                result.add(toExampleInfo(entry));
-
-                if (result.size() >= maxResults) {
-                    break;
+                total += group.getValue().size();
+                groups.add(new GroupInfo(
+                        group.getKey(),
+                        ExampleHelper.getGroupTitle(group.getKey()),
+                        ExampleHelper.getGroupIntro(group.getKey()),
+                        group.getValue().size()));
+                for (JsonObject entry : group.getValue()) {
+                    if (result.size() < maxResults) {
+                        result.add(toExampleInfo(entry));
+                    }
                 }
             }
 
-            return new ExampleListResult(result.size(), result);
+            return new ExampleListResult(result.size(), total, groups, result);
         } catch (Throwable e) {
             throw new ToolCallException(
                     "Failed to list examples (" + e.getClass().getName() + "): 
" + e.getMessage(), null);
@@ -136,7 +150,10 @@ public class ExampleTools {
                 entry.getString("title"),
                 entry.getString("description"),
                 entry.getString("level"),
+                ExampleHelper.getOrder(entry) == Integer.MAX_VALUE ? null : 
ExampleHelper.getOrder(entry),
                 tags != null ? new ArrayList<>(tags) : List.of(),
+                ExampleHelper.getTeaches(entry),
+                ExampleHelper.getInfraServices(entry),
                 ExampleHelper.isBundled(entry),
                 ExampleHelper.requiresDocker(entry),
                 ExampleHelper.getFiles(entry));
@@ -144,11 +161,15 @@ public class ExampleTools {
 
     // Result records
 
-    public record ExampleListResult(int count, List<ExampleInfo> examples) {
+    public record ExampleListResult(int count, int total, List<GroupInfo> 
groups, List<ExampleInfo> examples) {
+    }
+
+    public record GroupInfo(String level, String title, String intro, int 
count) {
     }
 
-    public record ExampleInfo(String name, String title, String description, 
String level,
-            List<String> tags, boolean bundled, boolean requiresDocker, 
List<String> files) {
+    public record ExampleInfo(String name, String title, String description, 
String level, Integer order,
+            List<String> tags, Map<String, List<String>> teaches, List<String> 
infraServices,
+            boolean bundled, boolean requiresDocker, List<String> files) {
     }
 
     public record ExampleFileResult(String example, String file, String 
content, String githubUrl) {
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
new file mode 100644
index 000000000000..05a14ce4e299
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.Comparator;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ExampleToolsTest {
+
+    @Test
+    void listsTheWholeLadderInReadingOrderWithoutArguments() {
+        ExampleTools.ExampleListResult all = new 
ExampleTools().camel_catalog_examples(null, null, null);
+
+        assertThat(all.count()).isEqualTo(all.total());
+        assertThat(all.groups()).isNotEmpty();
+        List<String> levels = 
all.groups().stream().map(ExampleTools.GroupInfo::level).toList();
+        assertThat(levels).isSubsetOf(ExampleHelper.getGroupOrder());
+        assertThat(levels).isSortedAccordingTo((a, b) -> Integer.compare(
+                ExampleHelper.getGroupOrder().indexOf(a), 
ExampleHelper.getGroupOrder().indexOf(b)));
+        assertThat(levels.get(0)).isEqualTo("quick-start");
+        ExampleTools.GroupInfo first = all.groups().get(0);
+        
assertThat(first.title()).isEqualTo(ExampleHelper.getGroupTitle("quick-start"));
+        assertThat(first.intro()).isNotBlank();
+
+        // the examples follow the groups in order
+        List<String> exampleLevels = 
all.examples().stream().map(ExampleTools.ExampleInfo::level).distinct().toList();
+        assertThat(exampleLevels).isEqualTo(levels);
+        
assertThat(all.examples().stream().map(ExampleTools.ExampleInfo::name)).contains("quick-start/timer-log");
+    }
+
+    @Test
+    void filtersOneGroupAndReturnsWhatTheExamplesTeach() {
+        ExampleTools.ExampleListResult run = new 
ExampleTools().camel_catalog_examples(null, "run", null);
+
+        assertThat(run.groups()).hasSize(1);
+        assertThat(run.groups().get(0).level()).isEqualTo("run");
+        assertThat(run.groups().get(0).count()).isEqualTo(run.total());
+        assertThat(run.examples()).allMatch(e -> "run".equals(e.level()));
+        assertThat(run.examples()).isSortedAccordingTo(
+                Comparator.comparingInt(e -> e.order() != null ? e.order() : 
Integer.MAX_VALUE));
+        assertThat(run.examples()).anyMatch(e -> !e.teaches().isEmpty());
+    }
+
+    @Test
+    void searchesByNameAndHonoursTheLimit() {
+        ExampleTools.ExampleListResult kafka = new 
ExampleTools().camel_catalog_examples("kafka", null, 1);
+
+        assertThat(kafka.total()).isGreaterThanOrEqualTo(1);
+        assertThat(kafka.count()).isEqualTo(1);
+        assertThat(kafka.examples().get(0).name()).contains("kafka");
+        assertThat(kafka.examples().get(0).infraServices()).contains("kafka");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
index f34f289e88e1..2a028ff04da1 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
@@ -500,14 +500,12 @@ final class TuiToolDefinitions {
     private static void addExampleTools(List<ToolDef> tools) {
         tools.add(toToolDef(toolDef(
                 "tui_list_examples",
-                "Returns the list of available bundled Camel examples as 
structured JSON. "
-                                     + "Each example has: name, title, 
description, level, category, tags, "
-                                     + "bundled, requiresDocker, 
infraServices. "
-                                     + "Use the 'name' field with 
tui_run_example to launch one.",
-                Map.of("filter", propDef("string",
-                        "Case-insensitive substring filter on name, title, 
description, level, or tags"),
-                        "level", propDef("string",
-                                "Filter by difficulty level: beginner, 
intermediate, or advanced")))));
+                "Lists the Camel examples by group in reading order: groups 
(level, title, intro, count) and "
+                                     + "examples (name, title, description, 
level, order, tags, teaches, bundled, "
+                                     + "requiresDocker, infraServices). Use 
name with tui_run_example.",
+                Map.of("filter", propDef("string", "Substring filter on name, 
title, description or tags"),
+                        "level", propDef("string", "One group, e.g. 
quick-start, run, ai"),
+                        "limit", propDef("integer", "Max examples (default 
50)")))));
         tools.add(toToolDef(toolDef(
                 "tui_run_example",
                 "Launches a named bundled example as a background process. "
@@ -515,7 +513,7 @@ final class TuiToolDefinitions {
                                    + "Automatically starts required infra 
services (Docker containers) if needed. "
                                    + "Use tui_list_examples to discover 
available example names.",
                 Map.of("name", propDef("string",
-                        "Example name from the catalog (e.g. 
'beginner/timer-log', 'ai/ollama')"),
+                        "Example name from the catalog (e.g. 
'quick-start/timer-log')"),
                         "profile", propDef("string",
                                 "Camel profile to use (e.g. 'dev'). 
Optional.")),
                 List.of("name"))));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 107be4c2a77b..882c207a2a23 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -1708,39 +1708,59 @@ class TuiToolRegistry {
 
         String filter = args.get("filter") instanceof String v ? v : null;
         String level = args.get("level") instanceof String v ? v : null;
+        int limit = args.get("limit") instanceof Number n && n.intValue() > 0 
? n.intValue() : 50;
 
         List<JsonObject> filtered = catalog;
         if (filter != null && !filter.isEmpty()) {
             filtered = ExampleHelper.filterExamples(filtered, filter);
         }
-        if (level != null && !level.isEmpty()) {
-            String lowerLevel = level.toLowerCase();
-            filtered = filtered.stream()
-                    .filter(e -> 
lowerLevel.equals(e.getStringOrDefault("level", "")))
-                    .toList();
-        }
 
+        JsonArray groups = new JsonArray();
         JsonArray examples = new JsonArray();
-        for (JsonObject entry : filtered) {
-            JsonObject ex = new JsonObject();
-            ex.put("name", entry.getStringOrDefault("name", ""));
-            ex.put("title", entry.getStringOrDefault("title", ""));
-            ex.put("description", entry.getStringOrDefault("description", ""));
-            ex.put("level", entry.getStringOrDefault("level", ""));
-            ex.put("category", ExampleHelper.getCategory(entry));
-            ex.put("tags", toJsonArray(
-                    entry.get("tags") instanceof java.util.Collection<?> c
-                            ? c.stream().map(Object::toString).toList()
-                            : List.of()));
-            ex.put("bundled", ExampleHelper.isBundled(entry));
-            ex.put("requiresDocker", ExampleHelper.requiresDocker(entry));
-            ex.put("infraServices", 
toJsonArray(ExampleHelper.getInfraServices(entry)));
-            examples.add(ex);
+        int total = 0;
+        for (Map.Entry<String, List<JsonObject>> group : 
ExampleHelper.groupByLevel(filtered).entrySet()) {
+            if (level != null && !level.isEmpty() && 
!group.getKey().equalsIgnoreCase(level)) {
+                continue;
+            }
+            total += group.getValue().size();
+            JsonObject g = new JsonObject();
+            g.put("level", group.getKey());
+            g.put("title", ExampleHelper.getGroupTitle(group.getKey()));
+            g.put("intro", ExampleHelper.getGroupIntro(group.getKey()));
+            g.put("count", group.getValue().size());
+            groups.add(g);
+            for (JsonObject entry : group.getValue()) {
+                if (examples.size() >= limit) {
+                    continue;
+                }
+                JsonObject ex = new JsonObject();
+                ex.put("name", entry.getStringOrDefault("name", ""));
+                ex.put("title", entry.getStringOrDefault("title", ""));
+                ex.put("description", entry.getStringOrDefault("description", 
""));
+                ex.put("level", entry.getStringOrDefault("level", ""));
+                if (ExampleHelper.getOrder(entry) != Integer.MAX_VALUE) {
+                    ex.put("order", ExampleHelper.getOrder(entry));
+                }
+                ex.put("category", ExampleHelper.getCategory(entry));
+                ex.put("tags", toJsonArray(
+                        entry.get("tags") instanceof java.util.Collection<?> c
+                                ? c.stream().map(Object::toString).toList()
+                                : List.of()));
+                JsonObject teaches = new JsonObject();
+                ExampleHelper.getTeaches(entry).forEach((k, v) -> 
teaches.put(k, toJsonArray(v)));
+                ex.put("teaches", teaches);
+                ex.put("bundled", ExampleHelper.isBundled(entry));
+                ex.put("requiresDocker", ExampleHelper.requiresDocker(entry));
+                ex.put("infraServices", 
toJsonArray(ExampleHelper.getInfraServices(entry)));
+                examples.add(ex);
+            }
         }
 
         JsonObject result = new JsonObject();
+        result.put("groups", groups);
         result.put("examples", examples);
-        result.put("totalCount", examples.size());
+        result.put("count", examples.size());
+        result.put("total", total);
         return Jsoner.serialize(result);
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistryLaunchTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistryLaunchTest.java
index 37eaa8aa3847..e7f5be0a9f52 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistryLaunchTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistryLaunchTest.java
@@ -88,4 +88,28 @@ class TuiToolRegistryLaunchTest {
 
         assertTrue(registry.execute("tui_run_example", 
UNKNOWN_EXAMPLE).contains("Launching examples is not available"));
     }
+
+    @Test
+    void listExamplesGroupsTheLadder() throws Exception {
+        TuiToolRegistry registry = new TuiToolRegistry(bareFacade());
+
+        String all = registry.execute("tui_list_examples", Map.of());
+        assertTrue(all.contains("\"groups\""), all.substring(0, Math.min(200, 
all.length())));
+        assertTrue(all.indexOf("\"level\":\"quick-start\"") < 
all.indexOf("\"level\":\"run\""),
+                "quick-start comes before run");
+        assertTrue(all.contains("timer-log"));
+        
assertTrue(all.contains("\"teaches\":{\"components\":[\"timer\",\"log\"]"), 
"teaches from the metadata");
+
+        String run = registry.execute("tui_list_examples", Map.of("level", 
"run"));
+        assertTrue(run.contains("\"level\":\"run\""));
+        assertTrue(!run.contains("\"level\":\"quick-start\""), "only the run 
group");
+
+        String one = registry.execute("tui_list_examples", Map.of("level", 
"run", "limit", 1));
+        assertTrue(one.contains("\"count\":1"), one);
+        assertTrue(one.contains("\"total\":" + 
run.substring(run.indexOf("\"total\":") + 8, run.indexOf("\"total\":") + 9)),
+                "total counts the whole group");
+
+        String two = registry.execute("tui_list_examples", Map.of("limit", 2));
+        assertTrue(two.contains("\"count\":2"), "the limit caps across all 
groups: " + two.substring(0, 80));
+    }
 }

Reply via email to