You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by sh...@apache.org on 2026/06/02 09:27:10 UTC

(unomi) branch master updated: UNOMI-879 Unified CRUD Karaf shell developer commands (includes UNOMI-920 YAML toString() debugging) (#755)

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

sergehuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/master by this push:
     new 956440cd4 UNOMI-879 Unified CRUD Karaf shell developer commands (includes UNOMI-920 YAML toString() debugging) (#755)
956440cd4 is described below

commit 956440cd45976afb6ba6b485a5605197eb8b6c12
Author: Serge Huber <sh...@apache.org>
AuthorDate: Tue Jun 2 11:27:04 2026 +0200

    UNOMI-879 Unified CRUD Karaf shell developer commands (includes UNOMI-920 YAML toString() debugging) (#755)
---
 .../apache/unomi/api/services/EventService.java    |   7 +
 .../apache/unomi/api/services/ProfileService.java  |   9 +
 .../apache/unomi/itests/graphql/GraphQLListIT.java |   7 +-
 .../services/impl/events/EventServiceImpl.java     |   5 +
 .../services/impl/profiles/ProfileServiceImpl.java |   5 +
 tools/shell-dev-commands/pom.xml                   |  14 +
 .../apache/unomi/shell/commands/ActionList.java    |  64 ---
 .../apache/unomi/shell/commands/ActionView.java    |  48 --
 .../apache/unomi/shell/commands/ConditionList.java |  64 ---
 .../apache/unomi/shell/commands/ConditionView.java |  50 --
 .../unomi/shell/commands/DeployDefinition.java     | 113 ----
 .../org/apache/unomi/shell/commands/EventList.java |  90 ---
 .../apache/unomi/shell/commands/EventSearch.java   |  99 ----
 .../org/apache/unomi/shell/commands/EventView.java |  60 --
 .../apache/unomi/shell/commands/ProfileList.java   |  84 ---
 .../apache/unomi/shell/commands/ProfileView.java   |  48 --
 .../org/apache/unomi/shell/commands/RuleList.java  | 108 ----
 .../apache/unomi/shell/commands/SegmentList.java   |  73 ---
 .../apache/unomi/shell/commands/SegmentRemove.java |  56 --
 .../apache/unomi/shell/commands/SegmentView.java   |  48 --
 .../apache/unomi/shell/commands/SessionList.java   |  79 ---
 .../unomi/shell/commands/UndeployDefinition.java   | 111 ----
 .../unomi/shell/dev/actions/UnomiCrudCommand.java  | 609 +++++++++++++++++++++
 .../unomi/shell/dev/commands/BaseCommand.java      |  53 ++
 .../unomi/shell/dev/commands/BaseListCommand.java  |  74 +++
 .../shell/dev/commands/BaseSimpleCommand.java      |  61 +++
 .../unomi/shell/dev/commands/CommandUtils.java     |  80 +++
 .../unomi/shell/dev/commands/DeployDefinition.java | 137 +++++
 .../commands/DeploymentCommandSupport.java         | 120 +++-
 .../unomi/shell/{ => dev}/commands/EventTail.java  |  16 +-
 .../{ => dev}/commands/ListCommandSupport.java     |  15 +-
 .../shell/dev/commands/RemoveCommandSupport.java   |  51 ++
 .../shell/{ => dev}/commands/RuleResetStats.java   |   7 +-
 .../unomi/shell/{ => dev}/commands/RuleTail.java   |  16 +-
 .../unomi/shell/{ => dev}/commands/RuleWatch.java  |  18 +-
 .../{ => dev}/commands/TailCommandSupport.java     |  12 +-
 .../unomi/shell/dev/commands/TailCommandUtils.java |  91 +++
 .../shell/dev/commands/UndeployDefinition.java     |  95 ++++
 .../commands/actions/ActionTypeCrudCommand.java    | 157 ++++++
 .../commands/campaigns/CampaignCrudCommand.java    | 151 +++++
 .../campaigns/CampaignEventCrudCommand.java        | 147 +++++
 .../conditions/ConditionTypeCrudCommand.java       | 153 ++++++
 .../dev/commands/consents/ConsentCrudCommand.java  | 255 +++++++++
 .../dev/commands/events/EventCrudCommand.java      | 167 ++++++
 .../shell/dev/commands/goals/GoalCrudCommand.java  | 143 +++++
 .../dev/commands/personas/PersonaCrudCommand.java  | 153 ++++++
 .../commands/profiles/ProfileAliasCrudCommand.java | 159 ++++++
 .../dev/commands/profiles/ProfileCrudCommand.java  | 186 +++++++
 .../properties/PropertyTypeCrudCommand.java        | 174 ++++++
 .../shell/dev/commands/rules/RuleCrudCommand.java  | 247 +++++++++
 .../commands/rules/RuleStatisticsCrudCommand.java  | 133 +++++
 .../dev/commands/scopes/ScopeCrudCommand.java      | 139 +++++
 .../dev/commands/scoring/ScoringCrudCommand.java   | 142 +++++
 .../dev/commands/segments/SegmentCrudCommand.java  | 195 +++++++
 .../dev/commands/sessions/SessionCrudCommand.java  | 128 +++++
 .../dev/commands/topics/TopicCrudCommand.java      | 120 ++++
 .../unomi/shell/dev/completers/BaseCompleter.java  |  86 +++
 .../unomi/shell/dev/completers/IdCompleter.java    | 156 ++++++
 .../completers/OperationCompleter.java}            |  27 +-
 .../unomi/shell/dev/completers/TypeCompleter.java  |  66 +++
 .../unomi/shell/dev/services/BaseCrudCommand.java  | 399 ++++++++++++++
 .../unomi/shell/dev/services/CrudCommand.java      | 206 +++++++
 62 files changed, 5300 insertions(+), 1286 deletions(-)

diff --git a/api/src/main/java/org/apache/unomi/api/services/EventService.java b/api/src/main/java/org/apache/unomi/api/services/EventService.java
index fb6d60d70..64ca1beeb 100644
--- a/api/src/main/java/org/apache/unomi/api/services/EventService.java
+++ b/api/src/main/java/org/apache/unomi/api/services/EventService.java
@@ -153,4 +153,11 @@ public interface EventService {
      * @param profileId identifier of the profile that we want to remove it's events
      */
     void removeProfileEvents(String profileId);
+
+    /**
+     * Deletes the event identified by the given identifier from persistence.
+     *
+     * @param eventIdentifier the unique identifier for the event
+     */
+    void deleteEvent(String eventIdentifier);
 }
diff --git a/api/src/main/java/org/apache/unomi/api/services/ProfileService.java b/api/src/main/java/org/apache/unomi/api/services/ProfileService.java
index 03da6ce9b..1895e9dbf 100644
--- a/api/src/main/java/org/apache/unomi/api/services/ProfileService.java
+++ b/api/src/main/java/org/apache/unomi/api/services/ProfileService.java
@@ -217,6 +217,15 @@ public interface ProfileService {
      */
     void removeProfileSessions(String profileId);
 
+    /**
+     * Deletes the session identified by the given identifier from persistence.
+     * Note: events belonging to this session are NOT removed; they remain in persistence
+     * with a dangling sessionId reference.
+     *
+     * @param sessionIdentifier the unique identifier for the session
+     */
+    void deleteSession(String sessionIdentifier);
+
     /**
      * Checks whether the specified profile and/or session satisfy the specified condition.
      *
diff --git a/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java
index 60a9a8507..66553e742 100644
--- a/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java
@@ -21,14 +21,10 @@ import org.apache.unomi.api.Profile;
 import org.apache.unomi.lists.UserList;
 import org.junit.Assert;
 import org.junit.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import java.util.Objects;
 
 public class GraphQLListIT extends BaseGraphQLIT {
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(GraphQLListIT.class);
-
     @Test
     public void testCRUD() throws Exception {
         Profile persistedProfile = null;
@@ -86,7 +82,6 @@ public class GraphQLListIT extends BaseGraphQLIT {
                         try (CloseableHttpResponse response = post("graphql/list/find-lists.json")) {
                             return ResponseContext.parse(response.getEntity());
                         } catch (Exception e) {
-                            LOGGER.warn("find-lists poll attempt failed: {}", e.getMessage());
                             return null;
                         }
                     },
@@ -108,7 +103,7 @@ public class GraphQLListIT extends BaseGraphQLIT {
             try (CloseableHttpResponse response = post("graphql/list/delete-list.json")) {
                 final ResponseContext context = ResponseContext.parse(response.getEntity());
 
-                Assert.assertTrue("testListId", context.getValue("data.cdp.deleteList"));
+                Assert.assertTrue("deleteList should return true", Boolean.TRUE.equals(context.getValue("data.cdp.deleteList")));
             }
         } finally {
             if (persistedProfile != null) {
diff --git a/services/src/main/java/org/apache/unomi/services/impl/events/EventServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/events/EventServiceImpl.java
index f345466c7..60680924e 100644
--- a/services/src/main/java/org/apache/unomi/services/impl/events/EventServiceImpl.java
+++ b/services/src/main/java/org/apache/unomi/services/impl/events/EventServiceImpl.java
@@ -336,4 +336,9 @@ public class EventServiceImpl implements EventService {
 
         persistenceService.removeByQuery(profileCondition,Event.class);
     }
+
+    @Override
+    public void deleteEvent(String eventIdentifier) {
+        persistenceService.remove(eventIdentifier, Event.class);
+    }
 }
diff --git a/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
index 9673a707c..e6b74c917 100644
--- a/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
+++ b/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
@@ -1015,6 +1015,11 @@ public class ProfileServiceImpl implements ProfileService, SynchronousBundleList
         persistenceService.removeByQuery(profileCondition, Session.class);
     }
 
+    @Override
+    public void deleteSession(String sessionIdentifier) {
+        persistenceService.remove(sessionIdentifier, Session.class);
+    }
+
     @Override
     public boolean matchCondition(Condition condition, Profile profile, Session session) {
         ParserHelper.resolveConditionType(definitionsService, condition, "profile " + profile.getItemId() + " matching");
diff --git a/tools/shell-dev-commands/pom.xml b/tools/shell-dev-commands/pom.xml
index 27b0b641f..807677a63 100644
--- a/tools/shell-dev-commands/pom.xml
+++ b/tools/shell-dev-commands/pom.xml
@@ -92,6 +92,8 @@
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient-osgi</artifactId>
+            <version>${httpclient-osgi.version}</version>
+            <type>bundle</type>
             <scope>provided</scope>
         </dependency>
         <dependency>
@@ -99,6 +101,10 @@
             <artifactId>commons-lang3</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-csv</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>junit</groupId>
@@ -115,6 +121,14 @@
                 <configuration>
                     <instructions>
                         <Karaf-Commands>*</Karaf-Commands>
+                        <Export-Package>
+                            org.apache.unomi.shell.dev.services,
+                            org.apache.unomi.shell.dev.commands
+                        </Export-Package>
+                        <_dsannotations>*</_dsannotations>
+                        <_dsannotations-options>inherit</_dsannotations-options>
+                        <_metatypeannotations>*</_metatypeannotations>
+                        <_metatypeannotations-options>version;nested</_metatypeannotations-options>
                     </instructions>
                 </configuration>
             </plugin>
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionList.java
deleted file mode 100644
index 7839cb7c8..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionList.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.actions.ActionType;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-@Command(scope = "unomi", name = "action-list", description = "This will list all the actions deployed in the Apache Unomi Context Server")
-@Service
-public class ActionList extends ListCommandSupport {
-
-    @Reference
-    DefinitionsService definitionsService;
-
-    @Override
-    protected String[] getHeaders() {
-        return new String[] {
-                "Id",
-                "Name",
-                "System tags"
-        };
-    }
-
-    @Override
-    protected DataTable buildDataTable() {
-        Collection<ActionType> allActions = definitionsService.getAllActionTypes();
-
-        DataTable dataTable = new DataTable();
-
-        for (ActionType actionType : allActions) {
-            ArrayList<Comparable> rowData = new ArrayList<>();
-            rowData.add(actionType.getItemId());
-            rowData.add(actionType.getMetadata().getName());
-            rowData.add(StringUtils.join(actionType.getMetadata().getSystemTags(), ","));
-            dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
-        }
-
-        dataTable.sort(new DataTable.SortCriteria(1, DataTable.SortOrder.ASCENDING));
-        return dataTable;
-    }
-
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionView.java
deleted file mode 100644
index 939e7f9d1..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionView.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.karaf.shell.api.action.Action;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.actions.ActionType;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-
-@Command(scope = "unomi", name = "action-view", description = "This will display a single action deployed in the Apache Unomi Context Server")
-@Service
-public class ActionView implements Action {
-
-    @Reference
-    DefinitionsService definitionsService;
-
-    @Argument(index = 0, name = "actionId", description = "The identifier for the action", required = true, multiValued = false)
-    String actionTypeIdentifier;
-
-    public Object execute() throws Exception {
-        ActionType actionType = definitionsService.getActionType(actionTypeIdentifier);
-        if (actionType == null) {
-            System.out.println("Couldn't find an action with id=" + actionTypeIdentifier);
-            return null;
-        }
-        String jsonRule = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(actionType);
-        System.out.println(jsonRule);
-        return null;
-    }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionList.java
deleted file mode 100644
index 83f6debc5..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionList.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.conditions.ConditionType;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-@Command(scope = "unomi", name = "condition-list", description = "This will list all the conditions deployed in the Apache Unomi Context Server")
-@Service
-public class ConditionList extends ListCommandSupport {
-
-    @Reference
-    DefinitionsService definitionsService;
-
-    @Override
-    protected String[] getHeaders() {
-        return new String[] {
-                "Id",
-                "Name",
-                "System tags"
-        };
-    }
-
-    @Override
-    protected DataTable buildDataTable() {
-        Collection<ConditionType> allConditionTypes = definitionsService.getAllConditionTypes();
-
-        DataTable dataTable = new DataTable();
-
-        for (ConditionType conditionType : allConditionTypes) {
-            ArrayList<Comparable> rowData = new ArrayList<>();
-            rowData.add(conditionType.getItemId());
-            rowData.add(conditionType.getMetadata().getName());
-            rowData.add(StringUtils.join(conditionType.getMetadata().getSystemTags(), ","));
-            dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
-        }
-
-        dataTable.sort(new DataTable.SortCriteria(1, DataTable.SortOrder.ASCENDING));
-        return dataTable;
-    }
-
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionView.java
deleted file mode 100644
index 03efe9d48..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionView.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.karaf.shell.api.action.Action;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.conditions.ConditionType;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-
-@Command(scope = "unomi", name = "condition-view", description = "This will display a single condition deployed in the Apache Unomi Context Server")
-@Service
-public class ConditionView implements Action {
-
-    @Reference
-    DefinitionsService definitionsService;
-
-    @Argument(index = 0, name = "conditionId", description = "The identifier for the condition", required = true, multiValued = false)
-    String conditionTypeIdentifier;
-
-    public Object execute() throws Exception {
-        ConditionType conditionType = definitionsService.getConditionType(conditionTypeIdentifier);
-        if (conditionType == null) {
-            System.out.println("Couldn't find an action with id=" + conditionTypeIdentifier);
-            return null;
-        }
-        String jsonRule = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(conditionType);
-        System.out.println(jsonRule);
-        return null;
-    }
-
-
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeployDefinition.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeployDefinition.java
deleted file mode 100644
index 9365de8a9..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeployDefinition.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Patch;
-import org.apache.unomi.api.PersonaWithSessions;
-import org.apache.unomi.api.PropertyType;
-import org.apache.unomi.api.actions.ActionType;
-import org.apache.unomi.api.campaigns.Campaign;
-import org.apache.unomi.api.conditions.ConditionType;
-import org.apache.unomi.api.goals.Goal;
-import org.apache.unomi.api.rules.Rule;
-import org.apache.unomi.api.segments.Scoring;
-import org.apache.unomi.api.segments.Segment;
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-
-import java.io.IOException;
-import java.net.URL;
-
-@Command(scope = "unomi", name = "deploy-definition", description = "This will deploy Unomi definitions contained in bundles")
-@Service
-public class DeployDefinition extends DeploymentCommandSupport {
-
-    public void processDefinition(String definitionType, URL definitionURL) {
-        try {
-            if (ALL_OPTION_LABEL.equals(definitionType)) {
-                String definitionURLString = definitionURL.toString();
-                for (String possibleDefinitionType : definitionTypes) {
-                    if (definitionURLString.contains(getDefinitionTypePath(possibleDefinitionType))) {
-                        definitionType = possibleDefinitionType;
-                        break;
-                    }
-                }
-                if (ALL_OPTION_LABEL.equals(definitionType)) {
-                    System.out.println("Couldn't resolve definition type for definition URL " + definitionURL);
-                    return;
-                }
-            }
-            boolean successful = true;
-            switch (definitionType) {
-                case CONDITION_DEFINITION_TYPE:
-                    ConditionType conditionType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, ConditionType.class);
-                    definitionsService.setConditionType(conditionType);
-                    break;
-                case ACTION_DEFINITION_TYPE:
-                    ActionType actionType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, ActionType.class);
-                    definitionsService.setActionType(actionType);
-                    break;
-                case GOAL_DEFINITION_TYPE:
-                    Goal goal = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Goal.class);
-                    goalsService.setGoal(goal);
-                    break;
-                case CAMPAIGN_DEFINITION_TYPE:
-                    Campaign campaign = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Campaign.class);
-                    goalsService.setCampaign(campaign);
-                    break;
-                case PERSONA_DEFINITION_TYPE:
-                    PersonaWithSessions persona = CustomObjectMapper.getObjectMapper().readValue(definitionURL, PersonaWithSessions.class);
-                    profileService.savePersonaWithSessions(persona);
-                    break;
-                case PROPERTY_DEFINITION_TYPE:
-                    PropertyType propertyType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, PropertyType.class);
-                    profileService.setPropertyTypeTarget(definitionURL, propertyType);
-                    profileService.setPropertyType(propertyType);
-                    break;
-                case RULE_DEFINITION_TYPE:
-                    Rule rule = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Rule.class);
-                    rulesService.setRule(rule);
-                    break;
-                case SEGMENT_DEFINITION_TYPE:
-                    Segment segment = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Segment.class);
-                    segmentService.setSegmentDefinition(segment);
-                    break;
-                case SCORING_DEFINITION_TYPE:
-                    Scoring scoring = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Scoring.class);
-                    segmentService.setScoringDefinition(scoring);
-                    break;
-                case PATCH_DEFINITION_TYPE:
-                    Patch patch = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Patch.class);
-                    patchService.patch(patch);
-                    break;
-                default:
-                    System.out.println("Unrecognized definition type:" + definitionType);
-                    successful = false;
-                    break;
-            }
-            if (successful) {
-                System.out.println("Predefined definition registered : " + definitionURL.getFile());
-            }
-        } catch (IOException e) {
-            System.out.println("Error while saving definition " + definitionURL);
-            System.out.println(e.getMessage());
-        }
-    }
-
-
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventList.java
deleted file mode 100644
index b996a7085..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventList.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Event;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.query.Query;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.EventService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-
-@Command(scope = "unomi", name = "event-list", description = "This commands lists the latest events updated in the Apache Unomi Context Server")
-@Service
-public class EventList extends ListCommandSupport {
-
-    @Reference
-    private EventService eventService;
-
-    @Reference
-    DefinitionsService definitionsService;
-
-    @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false)
-    int maxEntries = 100;
-
-    @Argument(index = 1, name = "eventType", description = "If specified, will filter the event list by the given event type", required = false, multiValued = false)
-    String eventType = null;
-
-    String[] columnHeaders = new String[] {
-            "ID",
-            "Type",
-            "Session",
-            "Profile",
-            "Timestamp",
-            "Scope",
-            "Persistent"
-    };
-
-    @Override
-    protected String[] getHeaders() {
-        return columnHeaders;
-    }
-
-    @Override
-    protected DataTable buildDataTable() {
-        Condition condition = new Condition(definitionsService.getConditionType("matchAllCondition"));
-        if (eventType != null) {
-            condition = new Condition(definitionsService.getConditionType("eventTypeCondition"));
-            condition.setParameter("eventTypeId", eventType);
-        }
-        Query query = new Query();
-        query.setLimit(maxEntries);
-        query.setCondition(condition);
-        query.setSortby("timeStamp:desc");
-        PartialList<Event> lastEvents = eventService.search(query);
-        DataTable dataTable = new DataTable();
-        for (Event event : lastEvents.getList()) {
-            ArrayList<Comparable> rowData = new ArrayList<>();
-            rowData.add(event.getItemId());
-            rowData.add(event.getEventType());
-            rowData.add(event.getSessionId());
-            rowData.add(event.getProfileId());
-            rowData.add(event.getTimeStamp().toString());
-            rowData.add(event.getScope());
-            rowData.add(Boolean.toString(event.isPersistent()));
-            dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
-        }
-        return dataTable;
-    }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventSearch.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventSearch.java
deleted file mode 100644
index 082f1d3d3..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventSearch.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Event;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.EventService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@Command(scope = "unomi", name = "event-search", description = "This commands search for profile events of a certain type by last timestamp in the Apache Unomi Context Server")
-@Service
-public class EventSearch extends ListCommandSupport  {
-    @Reference
-    private EventService eventService;
-
-    @Reference
-    DefinitionsService definitionsService;
-
-    @Argument(index = 0, name = "profile", description = "The identifier for the profile", required = true, multiValued = false)
-    String profileIdentifier;
-
-    @Argument(index = 1, name = "eventType", description = "The type of the event", required = false, multiValued = false)
-    String eventTypeId;
-
-    @Argument(index = 2, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false)
-    int maxEntries = 100;
-
-    String[] columnHeaders = new String[] {
-            "ID",
-            "Type",
-            "Session",
-            "Profile",
-            "Timestamp",
-            "Scope",
-            "Persistent"
-    };
-
-    @Override
-    protected String[] getHeaders() {
-        return columnHeaders;
-    }
-
-    @Override
-    protected DataTable buildDataTable() {
-        Condition booleanCondition = new Condition(definitionsService.getConditionType("booleanCondition"));
-        booleanCondition.setParameter("operator", "and");
-        List<Condition> subConditions = new ArrayList<>();
-        if (profileIdentifier != null) {
-            Condition eventProfileIdCondition = new Condition(definitionsService.getConditionType("eventPropertyCondition"));
-            eventProfileIdCondition.setParameter("propertyName", "profileId");
-            eventProfileIdCondition.setParameter("comparisonOperator", "equals");
-            eventProfileIdCondition.setParameter("propertyValue", profileIdentifier);
-            subConditions.add(eventProfileIdCondition);
-        }
-        if (eventTypeId != null) {
-            Condition eventTypeIdCondition = new Condition(definitionsService.getConditionType("eventTypeCondition"));
-            eventTypeIdCondition.setParameter("eventTypeId", eventTypeId);
-            subConditions.add(eventTypeIdCondition);
-        }
-        booleanCondition.setParameter("subConditions", subConditions);
-        PartialList<Event> lastEvents = eventService.searchEvents(booleanCondition, 0, maxEntries);
-        DataTable dataTable = new DataTable();
-        for (Event event : lastEvents.getList()) {
-            ArrayList<Comparable> rowData = new ArrayList<>();
-            rowData.add(event.getItemId());
-            rowData.add(event.getEventType());
-            rowData.add(event.getSessionId());
-            rowData.add(event.getProfileId());
-            rowData.add(event.getTimeStamp().toString());
-            rowData.add(event.getScope());
-            rowData.add(Boolean.toString(event.isPersistent()));
-            dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
-        }
-        return dataTable;
-    }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventView.java
deleted file mode 100644
index 10054ddd4..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventView.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.karaf.shell.api.action.Action;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Event;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.EventService;
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-
-@Command(scope = "unomi", name = "event-view", description = "This command will dump an Event as a JSON object")
-@Service
-public class EventView implements Action {
-
-    @Reference
-    EventService eventService;
-
-    @Reference
-    DefinitionsService definitionsService;
-
-    @Argument(index = 0, name = "event", description = "The identifier for the event", required = true, multiValued = false)
-    String eventIdentifier;
-
-    public Object execute() throws Exception {
-
-        Condition eventCondition = new Condition(definitionsService.getConditionType("eventPropertyCondition"));
-        eventCondition.setParameter("propertyName", "itemId");
-        eventCondition.setParameter("comparisonOperator", "equals");
-        eventCondition.setParameter("propertyValue", eventIdentifier);
-
-        PartialList<Event> matchingEvents = eventService.searchEvents(eventCondition, 0, 10);
-        if (matchingEvents == null || matchingEvents.getTotalSize() != 1) {
-            System.out.println("Couldn't find a single event with id=" + eventIdentifier + ". Maybe it wasn't a persistent event ?");
-            return null;
-        }
-        String jsonEvent = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(matchingEvents.get(0));
-        System.out.println(jsonEvent);
-        return null;
-    }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileList.java
deleted file mode 100644
index 24f3a2c97..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileList.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.Profile;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.query.Query;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.ProfileService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-
-@Command(scope = "unomi", name = "profile-list", description = "This commands lists the latest profiles updated in the Apache Unomi Context Server")
-@Service
-public class ProfileList extends ListCommandSupport {
-
-    @Reference
-    ProfileService profileService;
-
-    @Reference
-    DefinitionsService definitionsService;
-
-    @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false)
-    int maxEntries = 100;
-
-    @java.lang.Override
-    protected String[] getHeaders() {
-        return new String[] {
-                "ID",
-                "Scope",
-                "Segments",
-                "Consents",
-                "Last visit",
-                "Last update"
-        };
-    }
-
-    @java.lang.Override
-    protected DataTable buildDataTable() {
-        Query query = new Query();
-        query.setSortby("systemProperties.lastUpdated:desc,properties.lastVisit:desc");
-        query.setLimit(maxEntries);
-        Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition"));
-        query.setCondition(matchAllCondition);
-        PartialList<Profile> lastModifiedProfiles = profileService.search(query, Profile.class);
-        DataTable dataTable = new DataTable();
-        for (Profile profile : lastModifiedProfiles.getList()) {
-            ArrayList<Comparable> rowData = new ArrayList<>();
-            rowData.add(profile.getItemId());
-            rowData.add(profile.getScope());
-            rowData.add(StringUtils.join(profile.getSegments(), ","));
-            rowData.add(StringUtils.join(profile.getConsents().keySet(), ","));
-            rowData.add((String) profile.getProperty("lastVisit"));
-            if (profile.getSystemProperties() != null && profile.getSystemProperties().get("lastUpdated") != null) {
-                rowData.add((String) profile.getSystemProperties().get("lastUpdated"));
-            } else {
-                rowData.add("");
-            }
-            dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
-        }
-        return dataTable;
-    }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileView.java
deleted file mode 100644
index 89705790f..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileView.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.karaf.shell.api.action.Action;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Profile;
-import org.apache.unomi.api.services.ProfileService;
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-
-@Command(scope = "unomi", name = "profile-view", description = "This command will dump a profile as a JSON string")
-@Service
-public class ProfileView implements Action {
-
-    @Reference
-    ProfileService profileService;
-
-    @Argument(index = 0, name = "profile", description = "The identifier for the profile", required = true, multiValued = false)
-    String profileIdentifier;
-
-    public Object execute() throws Exception {
-        Profile profile = profileService.load(profileIdentifier);
-        if (profile == null) {
-            System.out.println("Couldn't find a profile with id=" + profileIdentifier);
-            return null;
-        }
-        String jsonProfile = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(profile);
-        System.out.println(jsonProfile);
-        return null;
-    }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleList.java
deleted file mode 100644
index de5a4d8c6..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleList.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Metadata;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.query.Query;
-import org.apache.unomi.api.rules.RuleStatistics;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.RulesService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-import java.util.Map;
-
-@Command(scope = "unomi", name = "rule-list", description = "This will list all the rules deployed in the Apache Unomi Context Server")
-@Service
-public class RuleList extends ListCommandSupport {
-
-    @Reference
-    RulesService rulesService;
-
-    @Reference
-    DefinitionsService definitionsService;
-
-    @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false)
-    int maxEntries = 100;
-
-    @Override
-    protected String[] getHeaders() {
-        return new String[] {
-                "Activated",
-                "Hidden",
-                "Read-only",
-                "Identifier",
-                "Scope",
-                "Name",
-                "Tags",
-                "System tags",
-                "Executions",
-                "Conditions [ms]",
-                "Actions [ms]"
-        };
-    }
-
-    @Override
-    protected DataTable buildDataTable() {
-        Query query = new Query();
-        Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition"));
-        query.setCondition(matchAllCondition);
-        query.setLimit(maxEntries);
-        PartialList<Metadata> ruleMetadatas = rulesService.getRuleMetadatas(query);
-        if (ruleMetadatas.getList().size() != ruleMetadatas.getTotalSize()) {
-            System.out.println("WARNING : Only the first " + ruleMetadatas.getPageSize() + " rules have been retrieved, there are " + ruleMetadatas.getTotalSize() + " rules registered in total. Use the maxEntries parameter to retrieve more rules");
-        }
-        Map<String,RuleStatistics> allRuleStatistics = rulesService.getAllRuleStatistics();
-
-        DataTable dataTable = new DataTable();
-        for (Metadata ruleMetadata : ruleMetadatas.getList()) {
-            ArrayList<Comparable> rowData = new ArrayList<>();
-            String ruleId = ruleMetadata.getId();
-            rowData.add(ruleMetadata.isEnabled() ? "x" : "");
-            rowData.add(ruleMetadata.isHidden() ? "x" : "");
-            rowData.add(ruleMetadata.isReadOnly() ? "x" : "");
-            rowData.add(ruleId);
-            rowData.add(ruleMetadata.getScope());
-            rowData.add(ruleMetadata.getName());
-            rowData.add(StringUtils.join(ruleMetadata.getTags(), ","));
-            rowData.add(StringUtils.join(ruleMetadata.getSystemTags(), ","));
-            RuleStatistics ruleStatistics = allRuleStatistics.get(ruleId);
-            if (ruleStatistics != null) {
-                rowData.add(ruleStatistics.getExecutionCount());
-                rowData.add(ruleStatistics.getConditionsTime());
-                rowData.add(ruleStatistics.getActionsTime());
-            } else {
-                rowData.add(0L);
-                rowData.add(0L);
-                rowData.add(0L);
-            }
-            dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
-        }
-        dataTable.sort(new DataTable.SortCriteria(9, DataTable.SortOrder.DESCENDING),
-                new DataTable.SortCriteria(10, DataTable.SortOrder.DESCENDING),
-                new DataTable.SortCriteria(5, DataTable.SortOrder.ASCENDING));
-        return dataTable;
-    }
-
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentList.java
deleted file mode 100644
index 5f8395137..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentList.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Metadata;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.services.SegmentService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-
-@Command(scope = "unomi", name = "segment-list", description = "This will list all the segments present in the Apache Unomi Context Server")
-@Service
-public class SegmentList extends ListCommandSupport {
-
-    @Reference
-    SegmentService segmentService;
-
-    @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false)
-    int maxEntries = 100;
-
-    @Override
-    protected String[] getHeaders() {
-        return new String[] {
-                "Enabled",
-                "Hidden",
-                "Id",
-                "Scope",
-                "Name",
-                "System tags"
-        };
-    }
-
-    @Override
-    protected DataTable buildDataTable() {
-        PartialList<Metadata> segmentMetadatas = segmentService.getSegmentMetadatas(0, maxEntries, null);
-
-        DataTable dataTable = new DataTable();
-        for (Metadata metadata : segmentMetadatas.getList()) {
-            ArrayList<Comparable> rowData = new ArrayList<>();
-            rowData.add(metadata.isEnabled() ? "x" : "");
-            rowData.add(metadata.isHidden() ? "x" : "");
-            rowData.add(metadata.getId());
-            rowData.add(metadata.getScope());
-            rowData.add(metadata.getName());
-            rowData.add(StringUtils.join(metadata.getSystemTags(), ","));
-            dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
-        }
-
-        dataTable.sort(new DataTable.SortCriteria(4, DataTable.SortOrder.ASCENDING));
-        return dataTable;
-    }
-
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentRemove.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentRemove.java
deleted file mode 100644
index 14b4efdf1..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentRemove.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.karaf.shell.api.action.Action;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.segments.DependentMetadata;
-import org.apache.unomi.api.services.SegmentService;
-
-@Command(scope = "unomi", name = "segment-remove", description = "Remove segments in the Apache Unomi Context Server")
-@Service
-public class SegmentRemove implements Action {
-
-    @Reference
-    SegmentService segmentService;
-
-    @Argument(index = 0, name = "segmentId", description = "The identifier for the segment", required = true, multiValued = false)
-    String segmentIdentifier;
-
-    @Argument(index = 1, name = "validate", description = "Check if the segment is used in goals or other segments", required = false, multiValued = false)
-    Boolean validate = true;
-
-
-    public Object execute() throws Exception {
-        DependentMetadata dependantMetadata = segmentService.removeSegmentDefinition(segmentIdentifier, validate);
-        if (!validate || (dependantMetadata.getSegments().isEmpty() && dependantMetadata.getScorings().isEmpty())) {
-            System.out.println("Segment " + segmentIdentifier + " successfully deleted");
-        } else if (validate) {
-            System.out.print("Segment " + segmentIdentifier + " could not be deleted because of the following dependents:");
-            if (!dependantMetadata.getScorings().isEmpty()) {
-                System.out.print(" scoring:" + dependantMetadata.getScorings());
-            }
-            if (!dependantMetadata.getSegments().isEmpty()) {
-                System.out.println(" segments:" + dependantMetadata.getSegments());
-            }
-        }
-        return null;
-    }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentView.java
deleted file mode 100644
index 9016aa9ed..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentView.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.karaf.shell.api.action.Action;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.segments.Segment;
-import org.apache.unomi.api.services.SegmentService;
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-
-@Command(scope = "unomi", name = "segment-view", description = "This will allows to view a segment in the Apache Unomi Context Server")
-@Service
-public class SegmentView implements Action {
-
-    @Reference
-    SegmentService segmentService;
-
-    @Argument(index = 0, name = "segmentId", description = "The identifier for the segment", required = true, multiValued = false)
-    String segmentIdentifier;
-
-    public Object execute() throws Exception {
-        Segment segment = segmentService.getSegmentDefinition(segmentIdentifier);
-        if (segment == null) {
-            System.out.println("Couldn't find a segment with id=" + segmentIdentifier);
-            return null;
-        }
-        String jsonRule = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(segment);
-        System.out.println(jsonRule);
-        return null;
-    }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SessionList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SessionList.java
deleted file mode 100644
index a3d0fda98..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SessionList.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.Session;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.query.Query;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.ProfileService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-
-@Command(scope = "unomi", name = "session-list", description = "This commands lists the latest sessions updated in the Apache Unomi Context Server")
-@Service
-public class SessionList extends ListCommandSupport {
-
-    @Reference
-    ProfileService profileService;
-
-    @Reference
-    DefinitionsService definitionsService;
-
-    @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false)
-    int maxEntries = 100;
-
-    @java.lang.Override
-    protected String[] getHeaders() {
-        return new String[] {
-                "ID",
-                "Scope",
-                "Last event",
-                "Duration",
-                "Profile",
-                "Timestamp"
-        };
-    }
-
-    @java.lang.Override
-    protected DataTable buildDataTable() {
-        Query query = new Query();
-        query.setSortby("lastEventDate:desc");
-        query.setLimit(maxEntries);
-        Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition"));
-        query.setCondition(matchAllCondition);
-        PartialList<Session> lastModifiedProfiles = profileService.searchSessions(query);
-        DataTable dataTable = new DataTable();
-        for (Session session : lastModifiedProfiles.getList()) {
-            ArrayList<Comparable> rowData = new ArrayList<>();
-            rowData.add(session.getItemId());
-            rowData.add(session.getScope());
-            rowData.add(session.getLastEventDate());
-            rowData.add(session.getDuration());
-            rowData.add(session.getProfileId());
-            rowData.add(session.getTimeStamp());
-            dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
-        }
-        return dataTable;
-    }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/UndeployDefinition.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/UndeployDefinition.java
deleted file mode 100644
index e672bff14..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/UndeployDefinition.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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.unomi.shell.commands;
-
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Patch;
-import org.apache.unomi.api.PersonaWithSessions;
-import org.apache.unomi.api.PropertyType;
-import org.apache.unomi.api.actions.ActionType;
-import org.apache.unomi.api.campaigns.Campaign;
-import org.apache.unomi.api.conditions.ConditionType;
-import org.apache.unomi.api.goals.Goal;
-import org.apache.unomi.api.rules.Rule;
-import org.apache.unomi.api.segments.Scoring;
-import org.apache.unomi.api.segments.Segment;
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-
-import java.io.IOException;
-import java.net.URL;
-
-@Command(scope = "unomi", name = "undeploy-definition", description = "This will undeploy definitions contained in bundles")
-@Service
-public class UndeployDefinition extends DeploymentCommandSupport {
-
-    public void processDefinition(String definitionType, URL definitionURL) {
-        try {
-            if (ALL_OPTION_LABEL.equals(definitionType)) {
-                String definitionURLString = definitionURL.toString();
-                for (String possibleDefinitionType : definitionTypes) {
-                    if (definitionURLString.contains(getDefinitionTypePath(possibleDefinitionType))) {
-                        definitionType = possibleDefinitionType;
-                        break;
-                    }
-                }
-                if (ALL_OPTION_LABEL.equals(definitionType)) {
-                    System.out.println("Couldn't resolve definition type for definition URL " + definitionURL);
-                    return;
-                }
-            }
-            boolean successful = true;
-            switch (definitionType) {
-                case CONDITION_DEFINITION_TYPE:
-                    ConditionType conditionType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, ConditionType.class);
-                    definitionsService.removeActionType(conditionType.getItemId());
-                    break;
-                case ACTION_DEFINITION_TYPE:
-                    ActionType actionType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, ActionType.class);
-                    definitionsService.removeActionType(actionType.getItemId());
-                    break;
-                case GOAL_DEFINITION_TYPE:
-                    Goal goal = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Goal.class);
-                    goalsService.removeGoal(goal.getItemId());
-                    break;
-                case CAMPAIGN_DEFINITION_TYPE:
-                    Campaign campaign = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Campaign.class);
-                    goalsService.removeCampaign(campaign.getItemId());
-                    break;
-                case PERSONA_DEFINITION_TYPE:
-                    PersonaWithSessions persona = CustomObjectMapper.getObjectMapper().readValue(definitionURL, PersonaWithSessions.class);
-                    profileService.delete(persona.getPersona().getItemId(), true);
-                    break;
-                case PROPERTY_DEFINITION_TYPE:
-                    PropertyType propertyType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, PropertyType.class);
-                    profileService.deletePropertyType(propertyType.getItemId());
-                    break;
-                case RULE_DEFINITION_TYPE:
-                    Rule rule = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Rule.class);
-                    rulesService.removeRule(rule.getItemId());
-                    break;
-                case SEGMENT_DEFINITION_TYPE:
-                    Segment segment = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Segment.class);
-                    segmentService.removeSegmentDefinition(segment.getItemId(), false);
-                    break;
-                case SCORING_DEFINITION_TYPE:
-                    Scoring scoring = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Scoring.class);
-                    segmentService.removeScoringDefinition(scoring.getItemId(), false);
-                    break;
-                case PATCH_DEFINITION_TYPE:
-                    Patch patch = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Patch.class);
-                    // patchService.patch(patch);
-                    break;
-                default:
-                    System.out.println("Unrecognized definition type: " + definitionType);
-                    successful = false;
-                    break;
-            }
-            if (successful) {
-                System.out.println("Predefined definition unregistered : " + definitionURL.getFile());
-            }
-        } catch (IOException e) {
-            System.out.println("Error while removing definition " + definitionURL);
-            System.out.println(e.getMessage());
-        }
-    }
-
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java
new file mode 100644
index 000000000..67b00d159
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java
@@ -0,0 +1,609 @@
+/*
+ * 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.unomi.shell.dev.actions;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.karaf.shell.api.action.*;
+import org.apache.karaf.shell.api.action.lifecycle.Init;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.table.ShellTable;
+
+import java.io.PrintStream;
+import org.apache.unomi.shell.dev.completers.IdCompleter;
+import org.apache.unomi.shell.dev.completers.OperationCompleter;
+import org.apache.unomi.shell.dev.completers.TypeCompleter;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+
+@Command(scope = "unomi", name = "crud", description = "Perform CRUD operations on Unomi objects")
+@Service
+public class UnomiCrudCommand implements Action {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(UnomiCrudCommand.class.getName());
+
+    private static final ObjectMapper OBJECT_MAPPER = CustomObjectMapper.getObjectMapper();
+
+    @Reference
+    private BundleContext bundleContext;
+
+    @Reference
+    private Session session;
+
+    @Argument(index = 0, name = "operation", description = "Operation to perform (create/read/update/delete/list/help)", required = true)
+    @Completion(OperationCompleter.class)
+    private String operation;
+
+    @Argument(index = 1, name = "type", description = "Object type", required = true)
+    @Completion(TypeCompleter.class)
+    private String type;
+
+    // Multi-valued argument that captures all remaining tokens after type
+    // ⚠️ IMPORTANT: Only the last argument (highest index) can be multi-valued in Karaf
+    // Since this is at index 2 (last argument), it can safely be multi-valued
+    // For create: remaining[0] = JSON/URL
+    // For read/delete: remaining[0] = ID
+    // For update: remaining[0] = ID, remaining[1] = JSON/URL
+    // For list: remaining contains all remaining tokens (--csv, -n, 50, etc.) for manual parsing
+    @Argument(index = 2, name = "remaining", description = "ID/JSON/URL (for create/read/update/delete) or remaining tokens (for list)", required = false, multiValued = true)
+    @Completion(IdCompleter.class)  // Could be enhanced to detect context
+    private List<String> remaining;
+
+    // Option fields for list operation (populated via manual parsing from remaining)
+    @Option(name = "--csv", description = "Output list in CSV format", required = false, multiValued = false)
+    private boolean csv;
+
+    @Option(name = "-n", aliases = "--max-entries", description = "Maximum number of entries to list", required = false)
+    private Integer maxEntries;
+
+    @Init
+    public void init() {
+        LOGGER.debug("UnomiCrudCommand init");
+    }
+
+    /**
+     * Check if a token is a max-entries option flag.
+     * 
+     * @param token the token to check
+     * @return true if the token is -n or --max-entries
+     */
+    private boolean isMaxEntriesOption(String token) {
+        return "-n".equals(token) || "--max-entries".equals(token);
+    }
+
+    /**
+     * Parse max-entries option value from the remaining list.
+     * Validates that the value is a positive integer.
+     * 
+     * @param remaining the remaining argument list
+     * @param index the index of the option flag
+     * @return the parsed integer value (must be > 0), or null if invalid/missing
+     */
+    private Integer parseMaxEntriesValue(List<String> remaining, int index) {
+        if (index + 1 >= remaining.size()) {
+            return null;
+        }
+        try {
+            int value = Integer.parseInt(remaining.get(index + 1));
+            // Only accept positive values
+            if (value <= 0) {
+                LOGGER.warn("Invalid max-entries value (must be positive): " + value);
+                return null;
+            }
+            return value;
+        } catch (NumberFormatException e) {
+            LOGGER.warn("Invalid number for max-entries option: " + remaining.get(index + 1));
+            return null;
+        }
+    }
+
+    /**
+     * Parse list-specific options from the remaining argument list.
+     * This implements Option 1: Simple Manual Parsing from the redesign proposal.
+     * 
+     * Note: If --csv was already set by Karaf's option parser (when placed before arguments),
+     * we preserve that value. Otherwise, we parse it from the remaining list.
+     * 
+     * @param remaining List of remaining tokens after type (e.g., ["--csv", "-n", "50"])
+     */
+    private void parseListOptions(List<String> remaining) {
+        // Preserve csv value if already set by Karaf's option parser (when --csv comes before arguments)
+        boolean csv = this.csv;
+        Integer maxEntries = this.maxEntries;
+        
+        if (remaining == null || remaining.isEmpty()) {
+            // Keep existing values if already set by Karaf
+            return;
+        }
+        
+        for (int i = 0; i < remaining.size(); i++) {
+            String token = remaining.get(i);
+            
+            if ("--csv".equals(token)) {
+                csv = true;
+            } else if (isMaxEntriesOption(token)) {
+                Integer value = parseMaxEntriesValue(remaining, i);
+                if (value != null) {
+                    maxEntries = value;
+                    i++; // Skip the next token as it's the value
+                }
+            }
+            // Ignore unknown tokens (could log warning)
+        }
+        
+        // Populate option fields
+        this.csv = csv;
+        this.maxEntries = maxEntries;
+    }
+
+    /**
+     * Check if remaining argument list has at least the specified number of non-empty elements.
+     * 
+     * @param remaining the remaining argument list
+     * @param minSize minimum number of elements required
+     * @return true if valid, false otherwise
+     */
+    private boolean hasMinimumRemainingArgs(List<String> remaining, int minSize) {
+        if (remaining == null || remaining.size() < minSize) {
+            return false;
+        }
+        for (int i = 0; i < minSize; i++) {
+            if (StringUtils.isBlank(remaining.get(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Parse JSON properties from remaining argument or URL with error handling.
+     * This method wraps parseProperties() and handles exceptions, providing consistent error messages.
+     * 
+     * @param jsonOrUrl JSON string or URL from remaining argument
+     * @param console Console for error output
+     * @return Map of properties, or null if parsing failed or error occurred
+     */
+    private Map<String, Object> parsePropertiesWithErrorHandling(String jsonOrUrl, PrintStream console) {
+        final String errorMsg = "Error: Failed to parse JSON or URL: " + jsonOrUrl;
+        try {
+            Map<String, Object> props = parseProperties(jsonOrUrl);
+            if (props == null) {
+                console.println(errorMsg);
+            }
+            return props;
+        } catch (Exception e) {
+            console.println(errorMsg);
+            console.println("Error details: " + e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * Strip surrounding quotes from a string if present.
+     * 
+     * @param str the string to process
+     * @return the string with quotes removed, or original if no quotes
+     */
+    private String stripQuotes(String str) {
+        if (StringUtils.isEmpty(str) || str.length() < 2) {
+            return str;
+        }
+        // Check for single quotes
+        if (str.charAt(0) == '\'' && str.charAt(str.length() - 1) == '\'') {
+            return str.substring(1, str.length() - 1);
+        }
+        // Check for double quotes
+        if (str.charAt(0) == '"' && str.charAt(str.length() - 1) == '"') {
+            return str.substring(1, str.length() - 1);
+        }
+        return str;
+    }
+
+    /**
+     * Check if a string is a valid URL by attempting to parse it as a URI.
+     * This method supports all URL schemes that Pax URL supports:
+     * - file:// (file protocol)
+     * - http://, https:// (HTTP/HTTPS protocols)
+     * - mvn: (Maven protocol)
+     * - war: (War protocol)
+     * - Any other valid URI scheme
+     * 
+     * The method uses Java's URI class to validate the scheme, which is more
+     * robust than simple string matching and supports all standard and custom schemes.
+     * 
+     * @param str the string to check
+     * @return true if the string is a valid URI with a scheme, false otherwise
+     */
+    private boolean isUrl(String str) {
+        if (StringUtils.isBlank(str)) {
+            return false;
+        }
+        
+        // JSON strings typically start with { or [, so they're not URLs
+        String trimmed = str.trim();
+        if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
+            return false;
+        }
+        
+        try {
+            URI uri = new URI(trimmed);
+            // A valid URI with a scheme is considered a URL
+            // getScheme() returns null for relative URIs, which are not URLs
+            return uri.getScheme() != null;
+        } catch (URISyntaxException e) {
+            // Not a valid URI, so not a URL
+            return false;
+        }
+    }
+
+    /**
+     * Parse JSON from a file URL.
+     * 
+     * @param fileUrl the file:// URL
+     * @return the parsed JSON as a Map
+     * @throws Exception if there's an error reading or parsing the file
+     */
+    private Map<String, Object> parseFileUrl(String fileUrl) throws Exception {
+        URI uri = new URI(fileUrl);
+        String scheme = uri.getScheme();
+        
+        if (!"file".equals(scheme)) {
+            throw new IllegalArgumentException("Expected file:// URL, got: " + fileUrl);
+        }
+        
+        // Handle file:// URLs - getPath() handles both file:///path and file://path
+        String filePath = uri.getPath();
+        if (filePath == null || filePath.isEmpty()) {
+            throw new IllegalArgumentException("Invalid file URL: " + fileUrl);
+        }
+        
+        @SuppressWarnings("unchecked")
+        Map<String, Object> result = OBJECT_MAPPER.readValue(Files.readString(Paths.get(filePath)), Map.class);
+        return result;
+    }
+
+    /**
+     * Parse JSON properties from remaining argument or URL.
+     * Supports:
+     * - Inline JSON string: {"itemId":"test"} (quoted or unquoted)
+     * - File URL: file:///path/to/file.json
+     * - HTTP/HTTPS URL: http://example.com/data.json (not yet implemented)
+     * - Maven URL: mvn:groupId/artifactId/version (not yet implemented)
+     * - War URL: war:file://path/to.war (not yet implemented)
+     * - Any other Pax URL supported scheme (not yet implemented)
+     * 
+     * Note: If JSON is quoted in the command (e.g., '{"itemId":"test"}'), 
+     * the Gogo parser will strip the quotes before passing to this method.
+     * This method handles both quoted and unquoted JSON strings.
+     * 
+     * @param jsonOrUrl JSON string or URL from remaining argument
+     * @return Map of properties, or null if invalid
+     * @throws Exception if there's an error parsing the JSON or reading the URL
+     */
+    private Map<String, Object> parseProperties(String jsonOrUrl) throws Exception {
+        if (StringUtils.isBlank(jsonOrUrl)) {
+            return null;
+        }
+        
+        String trimmed = stripQuotes(StringUtils.trim(jsonOrUrl));
+        
+        if (isUrl(trimmed)) {
+            URI uri = new URI(trimmed);
+            String scheme = uri.getScheme();
+            
+            if ("file".equals(scheme)) {
+                return parseFileUrl(trimmed);
+            } else {
+                // Other URL schemes (http, https, mvn, war, etc.) are not yet supported
+                // In the future, we could use Pax URL's URLStreamHandler to resolve these
+                throw new UnsupportedOperationException(
+                    "URL scheme '" + scheme + "' is not yet supported. " +
+                    "Currently only file:// URLs are supported. Use file:// or inline JSON.");
+            }
+        }
+        
+        // Treat as inline JSON
+        @SuppressWarnings("unchecked")
+        Map<String, Object> result = OBJECT_MAPPER.readValue(trimmed, Map.class);
+        return result;
+    }
+
+    /**
+     * Validate that operation and type are provided.
+     * 
+     * @param console console for error output
+     * @return true if valid, false otherwise
+     */
+    private boolean validateOperationAndType(PrintStream console) {
+        if (StringUtils.isBlank(operation)) {
+            console.println("Error: Operation is required");
+            console.println("Usage: unomi:crud <operation> <type> [remaining...]");
+            console.println("Available operations: create, read, update, delete, list, help");
+            return false;
+        }
+        
+        if (StringUtils.isBlank(type)) {
+            console.println("Error: Type is required");
+            console.println("Usage: unomi:crud <operation> <type> [remaining...]");
+            return false;
+        }
+        
+        return true;
+    }
+
+    /**
+     * Execute the appropriate handler for the given operation.
+     * 
+     * @param cmd the CrudCommand instance
+     * @param operationLower the lowercase operation name
+     * @param console console for output
+     * @return the result of the operation
+     * @throws Exception if the operation fails
+     */
+    private Object executeOperation(CrudCommand cmd, String operationLower, PrintStream console) throws Exception {
+        switch (operationLower) {
+            case "create":
+                return handleCreate(cmd, console);
+                
+            case "read":
+                return handleRead(cmd, console);
+                
+            case "update":
+                return handleUpdate(cmd, console);
+                
+            case "delete":
+                return handleDelete(cmd, console);
+                
+            case "list":
+                return handleList(cmd, console);
+                
+            case "help":
+                console.println("Properties for " + type + ":");
+                console.println(cmd.getPropertiesHelp());
+                return null;
+                
+            default:
+                console.println("Unknown operation: " + operation);
+                console.println("Available operations: create, read, update, delete, list, help");
+                return null;
+        }
+    }
+
+    /**
+     * Find and execute the CrudCommand for the given type.
+     * 
+     * @param console console for output
+     * @return true if a handler was found and executed, false otherwise
+     * @throws Exception if the operation fails
+     */
+    private boolean findAndExecuteCommand(PrintStream console) throws Exception {
+        ServiceReference<?>[] refs = bundleContext.getAllServiceReferences(CrudCommand.class.getName(), null);
+        if (refs == null) {
+            return false;
+        }
+        
+        String operationLower = operation.toLowerCase();
+        for (ServiceReference<?> ref : refs) {
+            CrudCommand cmd = (CrudCommand) bundleContext.getService(ref);
+            if (cmd == null) continue;
+            try {
+                if (cmd.getObjectType().equals(type)) {
+                    executeOperation(cmd, operationLower, console);
+                    return true;
+                }
+            } finally {
+                bundleContext.ungetService(ref);
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public Object execute() throws Exception {
+        PrintStream console = session.getConsole();
+        
+        if (!validateOperationAndType(console)) {
+            return null;
+        }
+        
+        boolean handlerFound = findAndExecuteCommand(console);
+        if (!handlerFound) {
+            console.println("No handler found for object type: " + type);
+        }
+        return null;
+    }
+
+    /**
+     * Validate that remaining argument list has at least one non-empty element for create operation.
+     * 
+     * @param remaining the remaining argument list
+     * @param console console for error output
+     * @return true if valid, false otherwise
+     */
+    private boolean validateCreateRemaining(List<String> remaining, PrintStream console) {
+        if (!hasMinimumRemainingArgs(remaining, 1)) {
+            console.println("Error: JSON string or URL is required for create operation");
+            console.println("Usage: unomi:crud create <type> <json|url>");
+            console.println("Example: unomi:crud create goal '{\"itemId\":\"test\",\"enabled\":true}'");
+            console.println("Example: unomi:crud create goal file:///path/to/file.json");
+            console.println("Note: Quote JSON strings to ensure they're treated as a single argument");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Handle create operation.
+     * Syntax: unomi:crud create <type> <json|url>
+     * remaining[0] = JSON string or URL
+     */
+    private Object handleCreate(CrudCommand cmd, PrintStream console) throws Exception {
+        if (!validateCreateRemaining(remaining, console)) {
+            return null;
+        }
+        
+        String jsonOrUrl = remaining.get(0);
+        Map<String, Object> createProps = parsePropertiesWithErrorHandling(jsonOrUrl, console);
+        if (createProps == null) {
+            return null;
+        }
+        
+        // Validate that we have at least some properties (empty JSON {} is not valid)
+        if (createProps.isEmpty()) {
+            console.println("Error: Empty JSON object is not valid. Please provide required properties.");
+            console.println("Usage: unomi:crud create <type> <json|url>");
+            return null;
+        }
+        
+        String newId = cmd.create(createProps);
+        if (newId == null) {
+            console.println("Error: Failed to create " + type + ". The create operation returned null.");
+            return null;
+        }
+        console.println("Created " + type + " with ID: " + newId);
+        return null;
+    }
+
+    /**
+     * Validate that remaining argument list has at least one non-empty element.
+     * 
+     * @param remaining the remaining argument list
+     * @param operation the operation name (for error messages)
+     * @param console console for error output
+     * @return true if valid, false otherwise
+     */
+    private boolean validateRemainingNotEmpty(List<String> remaining, String operation, PrintStream console) {
+        if (!hasMinimumRemainingArgs(remaining, 1)) {
+            console.println("Error: ID is required for " + operation + " operation");
+            console.println("Usage: unomi:crud " + operation + " <type> <id>");
+            console.println("Example: unomi:crud " + operation + " goal test-goal-123");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Handle read operation.
+     * Syntax: unomi:crud read <type> <id>
+     * remaining[0] = Object ID
+     */
+    private Object handleRead(CrudCommand cmd, PrintStream console) throws Exception {
+        if (!validateRemainingNotEmpty(remaining, "read", console)) {
+            return null;
+        }
+        
+        String id = remaining.get(0);
+        Map<String, Object> obj = cmd.read(id);
+        if (obj != null) {
+            console.println(OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj));
+        } else {
+            console.println(type + " not found with ID: " + id);
+        }
+        return null;
+    }
+
+    /**
+     * Handle update operation.
+     * Syntax: unomi:crud update <type> <id> <json|url>
+     * remaining[0] = Object ID
+     * remaining[1] = JSON string or URL
+     */
+    private Object handleUpdate(CrudCommand cmd, PrintStream console) throws Exception {
+        if (!hasMinimumRemainingArgs(remaining, 2)) {
+            console.println("Error: ID and JSON/URL are required for update operation");
+            console.println("Usage: unomi:crud update <type> <id> <json|url>");
+            console.println("Example: unomi:crud update goal test-goal-123 '{\"itemId\":\"test-goal-123\",\"enabled\":false}'");
+            console.println("Note: Quote JSON strings to ensure they're treated as a single argument");
+            return null;
+        }
+        
+        String id = remaining.get(0);
+        String jsonOrUrl = remaining.get(1);
+        
+        // hasMinimumRemainingArgs already ensures both id and jsonOrUrl are non-blank
+        Map<String, Object> updateProps = parsePropertiesWithErrorHandling(jsonOrUrl, console);
+        if (updateProps == null) {
+            return null;
+        }
+        
+        cmd.update(id, updateProps);
+        console.println("Updated " + type + " with ID: " + id);
+        return null;
+    }
+
+    /**
+     * Handle delete operation.
+     * Syntax: unomi:crud delete <type> <id>
+     * remaining[0] = Object ID
+     */
+    private Object handleDelete(CrudCommand cmd, PrintStream console) throws Exception {
+        if (!validateRemainingNotEmpty(remaining, "delete", console)) {
+            return null;
+        }
+        
+        String id = remaining.get(0);
+        cmd.delete(id);
+        console.println("Deleted " + type + " with ID: " + id);
+        return null;
+    }
+
+    /**
+     * Handle list operation.
+     * Syntax: unomi:crud list <type> [--csv] [-n <number>]
+     * remaining contains all remaining tokens (--csv, -n, 50, etc.) for manual parsing
+     */
+    private Object handleList(CrudCommand cmd, PrintStream console) throws Exception {
+        // Parse list-specific options from remaining argument
+        parseListOptions(remaining);
+        
+        String[] headers = cmd.getHeaders();
+        if (headers == null || headers.length == 0) {
+            console.println("Error: No headers available for " + type);
+            return null;
+        }
+        
+        // Ensure limit is positive (default to 100 if null or invalid)
+        int limit = (maxEntries != null && maxEntries > 0) ? maxEntries : 100;
+        
+        if (csv) {
+            // Generate proper CSV output using Apache Commons CSV
+            cmd.buildCsvOutput(console, headers, limit);
+        } else {
+            // Generate table output
+            ShellTable table = new ShellTable();
+            for (String header : headers) {
+                table.column(header);
+            }
+            cmd.buildRows(table, limit);
+            table.print(console, true);
+        }
+        return null;
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseCommand.java
new file mode 100644
index 000000000..a968aaff1
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseCommand.java
@@ -0,0 +1,53 @@
+/*
+ * 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.unomi.shell.dev.commands;
+
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.table.ShellTable;
+import org.apache.unomi.persistence.spi.PersistenceService;
+
+import java.io.IOException;
+import java.io.PrintStream;
+
+/**
+ * Base class for Unomi shell commands
+ */
+public abstract class BaseCommand implements Action {
+    protected static final int DEFAULT_ENTRIES = 100;
+
+    @Reference
+    protected PersistenceService persistenceService;
+
+    protected ShellTable buildTable() {
+        ShellTable table = new ShellTable();
+        table.column("ID");
+        table.column("Name");
+        table.column("Description");
+        return table;
+    }
+
+    protected void printTable(ShellTable table, Session session) {
+        table.print(session.getConsole());
+    }
+
+    protected boolean confirm(Session session, String message) throws IOException {
+        String response = session.readLine(message + " (y/n): ", null);
+        return response != null && response.toLowerCase().startsWith("y");
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseListCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseListCommand.java
new file mode 100644
index 000000000..f40b51a35
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseListCommand.java
@@ -0,0 +1,74 @@
+/*
+ * 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.unomi.shell.dev.commands;
+
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.table.ShellTable;
+import org.apache.unomi.api.Item;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.DefinitionsService;
+
+/**
+ * Base class for list commands
+ */
+public abstract class BaseListCommand<T extends Item> extends BaseCommand {
+
+    @Reference
+    protected DefinitionsService definitionsService;
+
+    @Reference
+    protected Session session;
+
+    @Option(name = "--max-entries", description = "Maximum number of entries to display", required = false)
+    protected int maxEntries = DEFAULT_ENTRIES;
+
+    @Option(name = "--sort-by", description = "Sort by field name", required = false)
+    protected String sortBy;
+
+    protected abstract Class<T> getItemType();
+
+    protected abstract void printItem(ShellTable table, T item);
+
+    @Override
+    public Object execute() throws Exception {
+        Query query = new Query();
+        query.setLimit(maxEntries);
+        query.setSortby(sortBy);
+
+        Condition condition = new Condition();
+        condition.setConditionType(definitionsService.getConditionType("matchAllCondition"));
+        query.setCondition(condition);
+
+        PartialList<T> items = persistenceService.query(query.getCondition(), query.getSortby(), getItemType(), query.getOffset(), query.getLimit());
+
+        ShellTable table = buildTable();
+        for (T item : items.getList()) {
+            printItem(table, item);
+        }
+        printTable(table, session);
+
+        return null;
+    }
+
+    public void setDefinitionsService(DefinitionsService definitionsService) {
+        this.definitionsService = definitionsService;
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseSimpleCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseSimpleCommand.java
new file mode 100644
index 000000000..1ef8ee99b
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseSimpleCommand.java
@@ -0,0 +1,61 @@
+/*
+ * 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.unomi.shell.dev.commands;
+
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.console.Session;
+
+import java.io.PrintStream;
+
+/**
+ * Base class for simple shell commands that provides common functionality
+ * for accessing Session and console output.
+ */
+public abstract class BaseSimpleCommand implements Action {
+
+    @Reference
+    protected Session session;
+
+    /**
+     * Get the console PrintStream from the session.
+     * 
+     * @return the console PrintStream
+     */
+    protected PrintStream getConsole() {
+        return session.getConsole();
+    }
+
+    /**
+     * Print a message to the console.
+     * 
+     * @param message the message to print
+     */
+    protected void println(String message) {
+        getConsole().println(message);
+    }
+
+    /**
+     * Print a formatted message to the console.
+     * 
+     * @param format the format string
+     * @param args the arguments
+     */
+    protected void printf(String format, Object... args) {
+        getConsole().printf(format, args);
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CommandUtils.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CommandUtils.java
new file mode 100644
index 000000000..44dfcbf82
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CommandUtils.java
@@ -0,0 +1,80 @@
+/*
+ * 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.unomi.shell.dev.commands;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Utility class for common command functionality.
+ */
+public final class CommandUtils {
+
+    /**
+     * Standard date format used across commands: "yyyy-MM-dd HH:mm:ss"
+     */
+    public static final String DATE_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss";
+
+    /**
+     * Thread-local SimpleDateFormat instance for date formatting.
+     * SimpleDateFormat is not thread-safe, so we use ThreadLocal to ensure thread safety.
+     */
+    private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = 
+        ThreadLocal.withInitial(() -> new SimpleDateFormat(DATE_FORMAT_PATTERN));
+
+    private CommandUtils() {
+        // Utility class - prevent instantiation
+    }
+
+    /**
+     * Format a date using the standard date format pattern.
+     * 
+     * @param date the date to format
+     * @return the formatted date string, or "-" if date is null
+     */
+    public static String formatDate(Date date) {
+        if (date == null) {
+            return "-";
+        }
+        return DATE_FORMAT.get().format(date);
+    }
+
+    /**
+     * Format a date using the standard date format pattern.
+     * 
+     * @param date the date to format
+     * @param nullValue the value to return if date is null
+     * @return the formatted date string, or nullValue if date is null
+     */
+    public static String formatDate(Date date, String nullValue) {
+        if (date == null) {
+            return nullValue;
+        }
+        return DATE_FORMAT.get().format(date);
+    }
+
+    /**
+     * Get a SimpleDateFormat instance for the standard pattern.
+     * Note: This returns a new instance each time. For thread-safe usage,
+     * prefer using formatDate() methods.
+     * 
+     * @return a SimpleDateFormat instance
+     */
+    public static SimpleDateFormat getDateFormat() {
+        return new SimpleDateFormat(DATE_FORMAT_PATTERN);
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeployDefinition.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeployDefinition.java
new file mode 100644
index 000000000..243e04e51
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeployDefinition.java
@@ -0,0 +1,137 @@
+/*
+ * 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.unomi.shell.dev.commands;
+
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.unomi.api.Patch;
+import org.apache.unomi.api.PersonaWithSessions;
+import org.apache.unomi.api.PropertyType;
+import org.apache.unomi.api.actions.ActionType;
+import org.apache.unomi.api.campaigns.Campaign;
+import org.apache.unomi.api.conditions.ConditionType;
+import org.apache.unomi.api.goals.Goal;
+import org.apache.unomi.api.rules.Rule;
+import org.apache.unomi.api.segments.Scoring;
+import org.apache.unomi.api.segments.Segment;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.URL;
+
+@Command(scope = "unomi", name = "deploy-definition", description = "This will deploy Unomi definitions contained in bundles")
+@Service
+public class DeployDefinition extends DeploymentCommandSupport {
+
+    public void processDefinition(String definitionType, URL definitionURL) {
+        try {
+            processDefinitionInternal(definitionType, definitionURL, getConsole(), "Predefined definition registered");
+        } catch (IOException e) {
+            handleDefinitionError(definitionURL, "saving", e);
+        }
+    }
+
+    protected void deployConditionType(URL definitionURL) throws IOException {
+        ConditionType conditionType = readDefinition(definitionURL, ConditionType.class);
+        definitionsService.setConditionType(conditionType);
+    }
+
+    protected void deployActionType(URL definitionURL) throws IOException {
+        ActionType actionType = readDefinition(definitionURL, ActionType.class);
+        definitionsService.setActionType(actionType);
+    }
+
+    protected void deployGoal(URL definitionURL) throws IOException {
+        Goal goal = readDefinition(definitionURL, Goal.class);
+        goalsService.setGoal(goal);
+    }
+
+    protected void deployCampaign(URL definitionURL) throws IOException {
+        Campaign campaign = readDefinition(definitionURL, Campaign.class);
+        goalsService.setCampaign(campaign);
+    }
+
+    protected void deployPersona(URL definitionURL) throws IOException {
+        PersonaWithSessions persona = readDefinition(definitionURL, PersonaWithSessions.class);
+        profileService.savePersonaWithSessions(persona);
+    }
+
+    protected void deployPropertyType(URL definitionURL) throws IOException {
+        PropertyType propertyType = readDefinition(definitionURL, PropertyType.class);
+        profileService.setPropertyTypeTarget(definitionURL, propertyType);
+        profileService.setPropertyType(propertyType);
+    }
+
+    protected void deployRule(URL definitionURL) throws IOException {
+        Rule rule = readDefinition(definitionURL, Rule.class);
+        rulesService.setRule(rule);
+    }
+
+    protected void deploySegment(URL definitionURL) throws IOException {
+        Segment segment = readDefinition(definitionURL, Segment.class);
+        segmentService.setSegmentDefinition(segment);
+    }
+
+    protected void deployScoring(URL definitionURL) throws IOException {
+        Scoring scoring = readDefinition(definitionURL, Scoring.class);
+        segmentService.setScoringDefinition(scoring);
+    }
+
+    protected void deployPatch(URL definitionURL) throws IOException {
+        Patch patch = readDefinition(definitionURL, Patch.class);
+        patchService.patch(patch);
+    }
+
+    @Override
+    protected boolean processDefinitionByType(String definitionType, URL definitionURL, PrintStream console) throws IOException {
+        switch (definitionType) {
+            case CONDITION_DEFINITION_TYPE:
+                deployConditionType(definitionURL);
+                return true;
+            case ACTION_DEFINITION_TYPE:
+                deployActionType(definitionURL);
+                return true;
+            case GOAL_DEFINITION_TYPE:
+                deployGoal(definitionURL);
+                return true;
+            case CAMPAIGN_DEFINITION_TYPE:
+                deployCampaign(definitionURL);
+                return true;
+            case PERSONA_DEFINITION_TYPE:
+                deployPersona(definitionURL);
+                return true;
+            case PROPERTY_DEFINITION_TYPE:
+                deployPropertyType(definitionURL);
+                return true;
+            case RULE_DEFINITION_TYPE:
+                deployRule(definitionURL);
+                return true;
+            case SEGMENT_DEFINITION_TYPE:
+                deploySegment(definitionURL);
+                return true;
+            case SCORING_DEFINITION_TYPE:
+                deployScoring(definitionURL);
+                return true;
+            case PATCH_DEFINITION_TYPE:
+                deployPatch(definitionURL);
+                return true;
+            default:
+                console.println("Unrecognized definition type:" + definitionType);
+                return false;
+        }
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeploymentCommandSupport.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeploymentCommandSupport.java
similarity index 65%
rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeploymentCommandSupport.java
rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeploymentCommandSupport.java
index 56db909ab..5081e3f26 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeploymentCommandSupport.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeploymentCommandSupport.java
@@ -14,14 +14,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.commands;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.karaf.shell.api.action.Action;
 import org.apache.karaf.shell.api.action.Argument;
 import org.apache.karaf.shell.api.action.lifecycle.Reference;
 import org.apache.karaf.shell.api.console.Session;
+
+import java.io.PrintStream;
 import org.apache.unomi.api.services.*;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
 import org.jline.reader.LineReader;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
@@ -33,7 +36,7 @@ import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
 
-public abstract class DeploymentCommandSupport implements Action {
+public abstract class DeploymentCommandSupport extends BaseSimpleCommand {
 
     public static final String ALL_OPTION_LABEL = "* (All)";
     @Reference
@@ -57,8 +60,6 @@ public abstract class DeploymentCommandSupport implements Action {
     @Reference
     BundleContext bundleContext;
 
-    @Reference
-    Session session;
 
     public static final String CONDITION_DEFINITION_TYPE = "conditions";
     public static final String ACTION_DEFINITION_TYPE = "actions";
@@ -102,6 +103,74 @@ public abstract class DeploymentCommandSupport implements Action {
 
     public abstract void processDefinition(String definitionType, URL definitionURL);
 
+    /**
+     * Process definition by type. Override in subclasses to provide specific implementation.
+     * 
+     * @param definitionType the type of definition
+     * @param definitionURL the URL of the definition
+     * @param console the console for output
+     * @return true if successful, false otherwise
+     * @throws IOException if there's an error reading the definition
+     */
+    protected boolean processDefinitionByType(String definitionType, URL definitionURL, PrintStream console) throws IOException {
+        return false;
+    }
+
+    /**
+     * Read a definition object from a URL using CustomObjectMapper.
+     * 
+     * @param definitionURL the URL to read from
+     * @param clazz the class of the object to read
+     * @param <T> the type of the object
+     * @return the read object
+     * @throws IOException if there's an error reading the definition
+     */
+    protected <T> T readDefinition(URL definitionURL, Class<T> clazz) throws IOException {
+        return CustomObjectMapper.getObjectMapper().readValue(definitionURL, clazz);
+    }
+
+    /**
+     * Handle errors that occur during definition processing.
+     * 
+     * @param definitionURL the URL of the definition that caused the error
+     * @param operation the operation being performed (e.g., "saving", "removing")
+     * @param e the exception that occurred
+     */
+    protected void handleDefinitionError(URL definitionURL, String operation, IOException e) {
+        PrintStream console = getConsole();
+        console.println("Error while " + operation + " definition " + definitionURL);
+        console.println(e.getMessage());
+    }
+
+    /**
+     * Internal method to process a definition. Handles common logic like resolving definition type.
+     * 
+     * @param definitionType the type of definition
+     * @param definitionURL the URL of the definition
+     * @param console the console for output
+     * @param successMessage the message to display on success
+     * @throws IOException if there's an error processing the definition
+     */
+    protected void processDefinitionInternal(String definitionType, URL definitionURL, PrintStream console, String successMessage) throws IOException {
+        if (ALL_OPTION_LABEL.equals(definitionType)) {
+            String definitionURLString = definitionURL.toString();
+            for (String possibleDefinitionType : definitionTypes) {
+                if (definitionURLString.contains(getDefinitionTypePath(possibleDefinitionType))) {
+                    definitionType = possibleDefinitionType;
+                    break;
+                }
+            }
+            if (ALL_OPTION_LABEL.equals(definitionType)) {
+                console.println("Couldn't resolve definition type for definition URL " + definitionURL);
+                return;
+            }
+        }
+        boolean successful = processDefinitionByType(definitionType, definitionURL, console);
+        if (successful) {
+            console.println(successMessage + " : " + definitionURL.getFile());
+        }
+    }
+
     public Object execute() throws Exception {
         List<Bundle> bundlesToUpdate;
         if ("*".equals(definitionType)) {
@@ -127,17 +196,23 @@ public abstract class DeploymentCommandSupport implements Action {
 
             String bundleAnswer = askUserWithAuthorizedAnswer(session, "Which bundle ?" + getValuesWithNumber(bundleSymbolicNames) + "\n",
                     IntStream.range(1,bundleSymbolicNames.size()+1).mapToObj(Integer::toString).collect(Collectors.toList()));
-            String selectedBundle = bundleSymbolicNames.get(new Integer(bundleAnswer)-1);
-            if (selectedBundle.equals(ALL_OPTION_LABEL)) {
-                bundlesToUpdate = bundles;
-            } else {
-                bundlesToUpdate = Collections.singletonList(bundles.get(new Integer(bundleAnswer) - 1));
+            try {
+                int bundleIndex = Integer.parseInt(bundleAnswer) - 1;
+                String selectedBundle = bundleSymbolicNames.get(bundleIndex);
+                if (selectedBundle.equals(ALL_OPTION_LABEL)) {
+                    bundlesToUpdate = bundles;
+                } else {
+                    bundlesToUpdate = Collections.singletonList(bundles.get(bundleIndex));
+                }
+            } catch (NumberFormatException e) {
+                println("Invalid selection: " + bundleAnswer);
+                return null;
             }
         } else {
             Bundle bundle = bundleContext.getBundle(bundleIdentifier);
 
             if (bundle == null) {
-                System.out.println("Couldn't find a bundle with id: " + bundleIdentifier);
+                println("Couldn't find a bundle with id: " + bundleIdentifier);
                 return null;
             }
 
@@ -146,27 +221,33 @@ public abstract class DeploymentCommandSupport implements Action {
 
         if (definitionType == null) {
             List<String> possibleDefinitionNames = definitionTypes.stream().filter((t) -> bundlesToUpdate.stream().anyMatch(b->b.findEntries(getDefinitionTypePath(t), "*.json", true) != null)).collect(Collectors.toList());
-            possibleDefinitionNames.add(ALL_OPTION_LABEL);
 
             if (possibleDefinitionNames.isEmpty()) {
-                System.out.println("Couldn't find definitions in bundle : " + bundlesToUpdate);
+                println("Couldn't find definitions in bundle : " + bundlesToUpdate);
                 return null;
             }
 
+            possibleDefinitionNames.add(ALL_OPTION_LABEL);
+
             String definitionTypeAnswer = askUserWithAuthorizedAnswer(session, "Which kind of definition do you want to load?" + getValuesWithNumber(possibleDefinitionNames) + "\n",
                     IntStream.range(1,possibleDefinitionNames.size()+1).mapToObj(Integer::toString).collect(Collectors.toList()));
-            definitionType = possibleDefinitionNames.get(new Integer(definitionTypeAnswer)-1);
+            try {
+                definitionType = possibleDefinitionNames.get(Integer.parseInt(definitionTypeAnswer) - 1);
+            } catch (NumberFormatException e) {
+                println("Invalid selection: " + definitionTypeAnswer);
+                return null;
+            }
         }
 
         if (!definitionTypes.contains(definitionType) && !ALL_OPTION_LABEL.equals(definitionType)) {
-            System.out.println("Invalid type '" + definitionType + "' , allowed values : " +definitionTypes);
+            println("Invalid type '" + definitionType + "' , allowed values : " +definitionTypes);
             return null;
         }
 
         String definitionTypePath = getDefinitionTypePath(definitionType);
         List<URL> definitionTypeURLs = bundlesToUpdate.stream().flatMap(b->b.findEntries(definitionTypePath, "*.json", true) != null ? Collections.list(b.findEntries(definitionTypePath, "*.json", true)).stream() : Stream.empty()).collect(Collectors.toList());
         if (definitionTypeURLs.isEmpty()) {
-            System.out.println("Couldn't find definitions in bundle with id: " + bundleIdentifier + " and definition path: " + definitionTypePath);
+            println("Couldn't find definitions in bundle with id: " + bundleIdentifier + " and definition path: " + definitionTypePath);
             return null;
         }
 
@@ -175,7 +256,12 @@ public abstract class DeploymentCommandSupport implements Action {
             definitionTypeFileNames.add(ALL_OPTION_LABEL);
             String fileNameAnswer = askUserWithAuthorizedAnswer(session, "Which file do you want to load ?" + getValuesWithNumber(definitionTypeFileNames) + "\n",
                     IntStream.range(1,definitionTypeFileNames.size()+1).mapToObj(Integer::toString).collect(Collectors.toList()));
-            fileName = definitionTypeFileNames.get(new Integer(fileNameAnswer)-1);
+            try {
+                fileName = definitionTypeFileNames.get(Integer.parseInt(fileNameAnswer) - 1);
+            } catch (NumberFormatException e) {
+                println("Invalid selection: " + fileNameAnswer);
+                return null;
+            }
         }
         if (ALL_OPTION_LABEL.equals(fileName)) {
             for (URL url : definitionTypeURLs) {
@@ -194,7 +280,7 @@ public abstract class DeploymentCommandSupport implements Action {
                 URL url = optionalURL.get();
                 processDefinition(definitionType, url);
             } else {
-                System.out.println("Couldn't find file " + fileName);
+                println("Couldn't find file " + fileName);
                 return null;
             }
         }
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventTail.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/EventTail.java
similarity index 81%
rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventTail.java
rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/EventTail.java
index 36ecdeceb..c4fd462c9 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventTail.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/EventTail.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.commands;
 
 import org.apache.karaf.shell.api.action.Argument;
 import org.apache.karaf.shell.api.action.Command;
@@ -24,7 +24,6 @@ import org.apache.unomi.api.services.EventListenerService;
 import org.apache.unomi.api.services.EventService;
 
 import java.io.PrintStream;
-import java.util.ArrayList;
 import java.util.List;
 
 @Command(scope = "unomi", name = "event-tail", description = "This will tail all the events coming into the Apache Unomi Context Server")
@@ -57,12 +56,12 @@ public class EventTail extends TailCommandSupport  {
 
     @Override
     public Object getListener() {
-        return new TailEventListener(session.getConsole());
+        return new TailEventListener(getConsole());
     }
 
     class TailEventListener implements EventListenerService {
 
-        PrintStream out;
+        private final PrintStream out;
 
         public TailEventListener(PrintStream out) {
             this.out = out;
@@ -78,14 +77,7 @@ public class EventTail extends TailCommandSupport  {
             if (!event.isPersistent() && !withInternal) {
                 return EventService.NO_CHANGE;
             }
-            List<String> eventInfo = new ArrayList<>();
-            eventInfo.add(event.getItemId());
-            eventInfo.add(event.getEventType());
-            eventInfo.add(event.getSessionId());
-            eventInfo.add(event.getProfileId());
-            eventInfo.add(event.getTimeStamp().toString());
-            eventInfo.add(event.getScope());
-            eventInfo.add(Boolean.toString(event.isPersistent()));
+            List<String> eventInfo = TailCommandUtils.extractEventInfo(event);
             outputLine(out, eventInfo);
             return EventService.NO_CHANGE;
         }
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ListCommandSupport.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/ListCommandSupport.java
similarity index 87%
rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ListCommandSupport.java
rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/ListCommandSupport.java
index dad45f10b..154c3c96d 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ListCommandSupport.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/ListCommandSupport.java
@@ -14,12 +14,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.commands;
 
 import org.apache.karaf.shell.api.action.Action;
 import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.console.Session;
 import org.apache.karaf.shell.support.table.Row;
 import org.apache.karaf.shell.support.table.ShellTable;
+
+import java.io.PrintStream;
 import org.apache.unomi.common.DataTable;
 
 import java.util.ArrayList;
@@ -29,6 +33,9 @@ import java.util.ArrayList;
  */
 public abstract class ListCommandSupport implements Action {
 
+    @Reference
+    protected Session session;
+
     @Option(name = "--csv", description = "Output table in CSV format", required = false, multiValued = false)
     boolean csv;
 
@@ -47,13 +54,13 @@ public abstract class ListCommandSupport implements Action {
     protected abstract DataTable buildDataTable();
 
     public Object execute() throws Exception {
-
         DataTable dataTable = buildDataTable();
 
         String[] headers = getHeaders();
+        PrintStream console = session.getConsole();
 
         if (csv) {
-            System.out.println(dataTable.toCSV(headers));
+            console.println(dataTable.toCSV(headers));
             return null;
         }
 
@@ -70,7 +77,7 @@ public abstract class ListCommandSupport implements Action {
             row.addContent(rowData);
         }
 
-        shellTable.print(System.out);
+        shellTable.print(console);
         return null;
     }
 
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RemoveCommandSupport.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RemoveCommandSupport.java
new file mode 100644
index 000000000..58009cca6
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RemoveCommandSupport.java
@@ -0,0 +1,51 @@
+/*
+ * 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.unomi.shell.dev.commands;
+
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.Option;
+
+import java.io.IOException;
+
+public abstract class RemoveCommandSupport extends BaseSimpleCommand {
+
+    @Option(name = "--force", description = "Force deletion without confirmation", required = false, multiValued = false)
+    boolean force;
+
+    public abstract Object doRemove() throws Exception;
+
+    public abstract String getResourceDescription();
+
+    @Override
+    public Object execute() throws Exception {
+        Object result = null;
+        // Prompt for confirmation
+        if (force || askForConfirmation("Are you sure you want to delete "+getResourceDescription()+" ? (yes/no): ")) {
+            result = doRemove();
+            println("Resource deleted successfully.");
+        } else {
+            println("Operation cancelled.");
+        }
+        return result;
+    }
+
+    private boolean askForConfirmation(String prompt) throws IOException {
+        String input = session.readLine(prompt, null);
+        return "yes".equals(input);
+    }
+
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleResetStats.java
similarity index 86%
copy from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java
copy to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleResetStats.java
index 7a228be9f..ad5c11aba 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleResetStats.java
@@ -14,9 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.commands;
 
-import org.apache.karaf.shell.api.action.Action;
 import org.apache.karaf.shell.api.action.Command;
 import org.apache.karaf.shell.api.action.lifecycle.Reference;
 import org.apache.karaf.shell.api.action.lifecycle.Service;
@@ -24,7 +23,7 @@ import org.apache.unomi.api.services.RulesService;
 
 @Command(scope = "unomi", name = "rule-reset-stats", description = "This command will reset the rule statistics")
 @Service
-public class RuleResetStats implements Action {
+public class RuleResetStats extends BaseSimpleCommand {
 
     @Reference
     RulesService rulesService;
@@ -32,7 +31,7 @@ public class RuleResetStats implements Action {
     @Override
     public Object execute() throws Exception {
         rulesService.resetAllRuleStatistics();
-        System.out.println("Rule statistics successfully reset.");
+        println("Rule statistics successfully reset.");
         return null;
     }
 }
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleTail.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleTail.java
similarity index 80%
rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleTail.java
rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleTail.java
index 6cf66b219..e6de275a9 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleTail.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleTail.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.commands;
 
 import org.apache.karaf.shell.api.action.Command;
 import org.apache.karaf.shell.api.action.lifecycle.Service;
@@ -23,7 +23,6 @@ import org.apache.unomi.api.rules.Rule;
 import org.apache.unomi.api.services.RuleListenerService;
 
 import java.io.PrintStream;
-import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -56,12 +55,12 @@ public class RuleTail extends TailCommandSupport {
 
     @Override
     public Object getListener() {
-        return new TailRuleListener(session.getConsole());
+        return new TailRuleListener(getConsole());
     }
 
     class TailRuleListener implements RuleListenerService {
 
-        PrintStream out;
+        private final PrintStream out;
 
         public TailRuleListener(PrintStream out) {
             this.out = out;
@@ -79,14 +78,7 @@ public class RuleTail extends TailCommandSupport {
 
         @Override
         public void onExecuteActions(Rule rule, Event event) {
-            List<String> ruleExecutionInfo = new ArrayList<>();
-            ruleExecutionInfo.add(rule.getItemId());
-            ruleExecutionInfo.add(rule.getMetadata().getName());
-            ruleExecutionInfo.add(event.getEventType());
-            ruleExecutionInfo.add(event.getSessionId());
-            ruleExecutionInfo.add(event.getProfileId());
-            ruleExecutionInfo.add(event.getTimeStamp().toString());
-            ruleExecutionInfo.add(event.getScope());
+            List<String> ruleExecutionInfo = TailCommandUtils.extractRuleExecutionInfo(rule, event);
             outputLine(out, ruleExecutionInfo);
         }
     }
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleWatch.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleWatch.java
similarity index 82%
rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleWatch.java
rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleWatch.java
index c94394455..63ba069e5 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleWatch.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleWatch.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.commands;
 
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.karaf.shell.api.action.Argument;
@@ -25,7 +25,6 @@ import org.apache.unomi.api.rules.Rule;
 import org.apache.unomi.api.services.RuleListenerService;
 
 import java.io.PrintStream;
-import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -62,12 +61,12 @@ public class RuleWatch extends TailCommandSupport {
 
     @Override
     public Object getListener() {
-        return new RuleWatchListener(session.getConsole());
+        return new RuleWatchListener(getConsole());
     }
 
     class RuleWatchListener implements RuleListenerService {
 
-        PrintStream out;
+        private final PrintStream out;
 
         public RuleWatchListener(PrintStream out) {
             this.out = out;
@@ -78,7 +77,6 @@ public class RuleWatch extends TailCommandSupport {
             populateRuleInfo(rule, event, "EVALUATE");
         }
 
-
         @Override
         public void onAlreadyRaised(AlreadyRaisedFor alreadyRaisedFor, Rule rule, Event event) {
             populateRuleInfo(rule, event, "AR " + alreadyRaisedFor.toString());
@@ -93,15 +91,7 @@ public class RuleWatch extends TailCommandSupport {
             if (!ArrayUtils.contains(ruleIds, rule.getItemId())) {
                 return;
             }
-            List<String> ruleExecutionInfo = new ArrayList<>();
-            ruleExecutionInfo.add(status);
-            ruleExecutionInfo.add(rule.getItemId());
-            ruleExecutionInfo.add(rule.getMetadata().getName());
-            ruleExecutionInfo.add(event.getEventType());
-            ruleExecutionInfo.add(event.getSessionId());
-            ruleExecutionInfo.add(event.getProfileId());
-            ruleExecutionInfo.add(event.getTimeStamp().toString());
-            ruleExecutionInfo.add(event.getScope());
+            List<String> ruleExecutionInfo = TailCommandUtils.extractRuleExecutionInfoWithStatus(rule, event, status);
             outputLine(out, ruleExecutionInfo);
         }
 
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/TailCommandSupport.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandSupport.java
similarity index 93%
rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/TailCommandSupport.java
rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandSupport.java
index f6af0328a..7de0d4451 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/TailCommandSupport.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandSupport.java
@@ -14,12 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.commands;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.karaf.shell.api.action.Action;
 import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.console.Session;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceRegistration;
 
@@ -28,15 +27,12 @@ import java.util.ArrayList;
 import java.util.Hashtable;
 import java.util.List;
 
-public abstract class TailCommandSupport implements Action {
+public abstract class TailCommandSupport extends BaseSimpleCommand {
 
     public abstract int[] getColumnSizes();
 
     public abstract String[] getColumnHeaders();
 
-    @Reference
-    Session session;
-
     @Reference
     BundleContext bundleContext;
 
@@ -74,12 +70,12 @@ public abstract class TailCommandSupport implements Action {
         }
         out.println(eventLine.toString());
     }
-    
+
     public abstract Object getListener();
 
     public Object execute() throws Exception {
         // Do not use System.out as it may write to the wrong console depending on the thread that calls our log handler
-        PrintStream out = session.getConsole();
+        PrintStream out = getConsole();
         out.flush();
         outputHeaders(out);
 
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandUtils.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandUtils.java
new file mode 100644
index 000000000..725fa6a5e
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandUtils.java
@@ -0,0 +1,91 @@
+/*
+ * 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.unomi.shell.dev.commands;
+
+import org.apache.unomi.api.Event;
+import org.apache.unomi.api.rules.Rule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class for common tail command functionality.
+ */
+public final class TailCommandUtils {
+
+    private TailCommandUtils() {
+        // Utility class - prevent instantiation
+    }
+
+    /**
+     * Extract event information into a list of strings for display.
+     * 
+     * @param event the event to extract information from
+     * @return list of event information strings
+     */
+    public static List<String> extractEventInfo(Event event) {
+        List<String> eventInfo = new ArrayList<>();
+        eventInfo.add(event.getItemId());
+        eventInfo.add(event.getEventType());
+        eventInfo.add(event.getSessionId());
+        eventInfo.add(event.getProfileId());
+        eventInfo.add(event.getTimeStamp() != null ? event.getTimeStamp().toString() : "-");
+        eventInfo.add(event.getScope());
+        eventInfo.add(Boolean.toString(event.isPersistent()));
+        return eventInfo;
+    }
+
+    /**
+     * Extract rule execution information into a list of strings for display.
+     *
+     * @param rule the rule to extract information from
+     * @param event the event associated with the rule execution
+     * @return list of rule execution information strings
+     */
+    public static List<String> extractRuleExecutionInfo(Rule rule, Event event) {
+        List<String> ruleExecutionInfo = new ArrayList<>();
+        ruleExecutionInfo.add(rule.getItemId());
+        ruleExecutionInfo.add(rule.getMetadata().getName());
+        ruleExecutionInfo.add(event.getEventType());
+        ruleExecutionInfo.add(event.getSessionId());
+        ruleExecutionInfo.add(event.getProfileId());
+        ruleExecutionInfo.add(event.getTimeStamp() != null ? event.getTimeStamp().toString() : "-");
+        ruleExecutionInfo.add(event.getScope());
+        return ruleExecutionInfo;
+    }
+
+    /**
+     * Extract rule execution information with status into a list of strings for display.
+     *
+     * @param rule the rule to extract information from
+     * @param event the event associated with the rule execution
+     * @param status the status of the rule execution (e.g., "EVALUATE", "EXECUTE", "AR ...")
+     * @return list of rule execution information strings with status as first element
+     */
+    public static List<String> extractRuleExecutionInfoWithStatus(Rule rule, Event event, String status) {
+        List<String> ruleExecutionInfo = new ArrayList<>();
+        ruleExecutionInfo.add(status);
+        ruleExecutionInfo.add(rule.getItemId());
+        ruleExecutionInfo.add(rule.getMetadata().getName());
+        ruleExecutionInfo.add(event.getEventType());
+        ruleExecutionInfo.add(event.getSessionId());
+        ruleExecutionInfo.add(event.getProfileId());
+        ruleExecutionInfo.add(event.getTimeStamp() != null ? event.getTimeStamp().toString() : "-");
+        ruleExecutionInfo.add(event.getScope());
+        return ruleExecutionInfo;
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java
new file mode 100644
index 000000000..530d003ff
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java
@@ -0,0 +1,95 @@
+/*
+ * 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.unomi.shell.dev.commands;
+
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.unomi.api.PersonaWithSessions;
+import org.apache.unomi.api.PropertyType;
+import org.apache.unomi.api.actions.ActionType;
+import org.apache.unomi.api.campaigns.Campaign;
+import org.apache.unomi.api.conditions.ConditionType;
+import org.apache.unomi.api.goals.Goal;
+import org.apache.unomi.api.rules.Rule;
+import org.apache.unomi.api.segments.Scoring;
+import org.apache.unomi.api.segments.Segment;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.URL;
+
+@Command(scope = "unomi", name = "undeploy-definition", description = "This will undeploy definitions contained in bundles")
+@Service
+public class UndeployDefinition extends DeploymentCommandSupport {
+
+    public void processDefinition(String definitionType, URL definitionURL) {
+        try {
+            processDefinitionInternal(definitionType, definitionURL, getConsole(), "Predefined definition unregistered");
+        } catch (IOException e) {
+            handleDefinitionError(definitionURL, "removing", e);
+        }
+    }
+
+    @Override
+    protected boolean processDefinitionByType(String definitionType, URL definitionURL, PrintStream console) throws IOException {
+        switch (definitionType) {
+            case CONDITION_DEFINITION_TYPE:
+                ConditionType conditionType = readDefinition(definitionURL, ConditionType.class);
+                definitionsService.removeConditionType(conditionType.getItemId());
+                return true;
+            case ACTION_DEFINITION_TYPE:
+                ActionType actionType = readDefinition(definitionURL, ActionType.class);
+                definitionsService.removeActionType(actionType.getItemId());
+                return true;
+            case GOAL_DEFINITION_TYPE:
+                Goal goal = readDefinition(definitionURL, Goal.class);
+                goalsService.removeGoal(goal.getItemId());
+                return true;
+            case CAMPAIGN_DEFINITION_TYPE:
+                Campaign campaign = readDefinition(definitionURL, Campaign.class);
+                goalsService.removeCampaign(campaign.getItemId());
+                return true;
+            case PERSONA_DEFINITION_TYPE:
+                PersonaWithSessions persona = readDefinition(definitionURL, PersonaWithSessions.class);
+                profileService.delete(persona.getPersona().getItemId(), true);
+                return true;
+            case PROPERTY_DEFINITION_TYPE:
+                PropertyType propertyType = readDefinition(definitionURL, PropertyType.class);
+                profileService.deletePropertyType(propertyType.getItemId());
+                return true;
+            case RULE_DEFINITION_TYPE:
+                Rule rule = readDefinition(definitionURL, Rule.class);
+                rulesService.removeRule(rule.getItemId());
+                return true;
+            case SEGMENT_DEFINITION_TYPE:
+                Segment segment = readDefinition(definitionURL, Segment.class);
+                segmentService.removeSegmentDefinition(segment.getItemId(), false);
+                return true;
+            case SCORING_DEFINITION_TYPE:
+                Scoring scoring = readDefinition(definitionURL, Scoring.class);
+                segmentService.removeScoringDefinition(scoring.getItemId(), false);
+                return true;
+            case PATCH_DEFINITION_TYPE:
+                console.println("Patch undeployment is not supported.");
+                return false;
+            default:
+                console.println("Unrecognized definition type: " + definitionType);
+                return false;
+        }
+    }
+
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java
new file mode 100644
index 000000000..319464aef
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java
@@ -0,0 +1,157 @@
+/*
+ * 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.unomi.shell.dev.commands.actions;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.Parameter;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Scope;
+import org.apache.unomi.api.actions.ActionType;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ScopeService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class ActionTypeCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+        "itemId", "name", "description", "scope", "tags", "systemTags", "parameters"
+    );
+
+    @Reference
+    private ScopeService scopeService;
+
+    @Override
+    public String getObjectType() {
+        return "actiontype";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[] {
+            "Identifier",
+            "Name",
+            "Description",
+            "Scope",
+            "Tags",
+            "System Tags",
+            "Parameters",
+            "Action Executor"
+        };
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        List<ActionType> actionTypes = new ArrayList<>(definitionsService.getAllActionTypes());
+        return paginateList(actionTypes, query);
+    }
+
+    @Override
+    protected String[] buildRow(Object item) {
+        ActionType actionType = (ActionType) item;
+        return new String[] {
+            actionType.getItemId(),
+            actionType.getMetadata().getName(),
+            actionType.getMetadata().getDescription(),
+            actionType.getMetadata().getScope(),
+            String.join(",", actionType.getMetadata().getTags()),
+            String.join(",", actionType.getMetadata().getSystemTags()),
+            String.join(",", actionType.getParameters().stream().map(Parameter::getId).collect(Collectors.toList())),
+            actionType.getActionExecutor()
+        };
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        ActionType actionType = OBJECT_MAPPER.convertValue(properties, ActionType.class);
+        definitionsService.setActionType(actionType);
+        return actionType.getItemId();
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        ActionType actionType = definitionsService.getActionType(id);
+        if (actionType != null) {
+            return OBJECT_MAPPER.convertValue(actionType, Map.class);
+        }
+        return null;
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        ActionType existing = definitionsService.getActionType(id);
+        if (existing == null) {
+            throw new IllegalArgumentException("Action type not found: " + id);
+        }
+
+        ActionType updated = OBJECT_MAPPER.convertValue(properties, ActionType.class);
+        updated.setItemId(id);
+        definitionsService.setActionType(updated);
+    }
+
+    @Override
+    public void delete(String id) {
+        definitionsService.removeActionType(id);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return String.join("\n",
+            "Required properties:",
+            "- itemId: Identifier for the action type",
+            "- name: Name of the action type",
+            "",
+            "Optional properties:",
+            "- description: Description of the action type",
+            "- scope: Scope of the action type",
+            "- tags: List of tags",
+            "- systemTags: List of system tags",
+            "- parameters: Map of parameter definitions"
+        );
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public List<String> completePropertyValue(String propertyName, String prefix) {
+        if ("scope".equals(propertyName)) {
+            return scopeService.getScopes().stream()
+                    .map(Scope::getItemId)
+                    .filter(id -> id.startsWith(prefix))
+                    .collect(Collectors.toList());
+        }
+        return Collections.emptyList();
+    }
+
+    public void setScopeService(ScopeService scopeService) {
+        this.scopeService = scopeService;
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java
new file mode 100644
index 000000000..c908f5320
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java
@@ -0,0 +1,151 @@
+/*
+ * 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.unomi.shell.dev.commands.campaigns;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.campaigns.Campaign;
+import org.apache.unomi.api.campaigns.CampaignDetail;
+import org.apache.unomi.api.goals.Goal;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.GoalsService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A command to perform CRUD operations on campaigns
+ */
+@Component(service = CrudCommand.class, immediate = true)
+public class CampaignCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+        "itemId", "name", "description", "scope", "startDate", "endDate", "cost", "currency", "primaryGoal", "goals", "entryCondition", "enabled"
+    );
+
+    @Reference
+    private GoalsService goalsService;
+
+    @Override
+    public String getObjectType() {
+        return "campaign";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[]{"ID", "Name", "Description", "Start Date", "End Date", "Cost", "Currency", "Primary Goal", "Enabled"};
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        return goalsService.getCampaignDetails(query);
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        CampaignDetail detail = (CampaignDetail) item;
+        Campaign campaign = detail.getCampaign();
+        String primaryGoalName = "";
+        if (campaign.getPrimaryGoal() != null) {
+            // Get the goal details to get its name
+            Goal primaryGoal = goalsService.getGoal(campaign.getPrimaryGoal());
+            if (primaryGoal != null) {
+                primaryGoalName = primaryGoal.getMetadata().getName();
+            }
+        }
+        return new Comparable[]{
+            campaign.getItemId(),
+            campaign.getMetadata().getName(),
+            campaign.getMetadata().getDescription(),
+            campaign.getStartDate() != null ? campaign.getStartDate().toString() : "",
+            campaign.getEndDate() != null ? campaign.getEndDate().toString() : "",
+            campaign.getCost() != null ? campaign.getCost().toString() : "",
+            campaign.getCurrency(),
+            primaryGoalName,
+            campaign.getMetadata().isEnabled()
+        };
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        Campaign campaign = goalsService.getCampaign(id);
+        if (campaign == null) {
+            return null;
+        }
+        return OBJECT_MAPPER.convertValue(campaign, Map.class);
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        Campaign campaign = OBJECT_MAPPER.convertValue(properties, Campaign.class);
+        goalsService.setCampaign(campaign);
+        return campaign.getItemId();
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        Campaign existingCampaign = goalsService.getCampaign(id);
+        if (existingCampaign == null) {
+            throw new IllegalArgumentException("Campaign not found with ID: " + id);
+        }
+
+        Campaign updatedCampaign = OBJECT_MAPPER.convertValue(properties, Campaign.class);
+        updatedCampaign.setItemId(id);
+        goalsService.setCampaign(updatedCampaign);
+    }
+
+    @Override
+    public void delete(String id) {
+        Campaign campaign = goalsService.getCampaign(id);
+        if (campaign != null) {
+            goalsService.removeCampaign(id);
+        }
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return String.join("\n",
+            "Required properties:",
+            "- itemId: The unique identifier of the campaign",
+            "- name: The name of the campaign",
+            "- description: The description of the campaign",
+            "",
+            "Optional properties:",
+            "- scope: The scope of the campaign (defaults to systemscope)",
+            "- startDate: The start date of the campaign (ISO-8601 format)",
+            "- endDate: The end date of the campaign (ISO-8601 format)",
+            "- cost: The cost of the campaign",
+            "- currency: The currency for the campaign cost",
+            "- primaryGoal: The primary goal of the campaign",
+            "- goals: List of goals associated with the campaign",
+            "- entryCondition: The condition that determines when a visitor enters the campaign",
+            "- enabled: Whether the campaign is enabled (true/false)"
+        );
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java
new file mode 100644
index 000000000..28ddb190a
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java
@@ -0,0 +1,147 @@
+/*
+ * 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.unomi.shell.dev.commands.campaigns;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.campaigns.events.CampaignEvent;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.GoalsService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A command to perform CRUD operations on campaign events
+ */
+@Component(service = CrudCommand.class, immediate = true)
+public class CampaignEventCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+        "itemId", "name", "description", "scope", "campaignId", "eventDate", "cost", "currency", "timezone"
+    );
+
+    @Reference
+    private GoalsService goalsService;
+
+    @Override
+    public String getObjectType() {
+        return "campaignevent";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[]{"ID", "Name", "Description", "Campaign ID", "Event Date", "Cost", "Currency", "Timezone"};
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        return goalsService.getEvents(query);
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        CampaignEvent event = (CampaignEvent) item;
+        return new Comparable[]{
+            event.getItemId(),
+            event.getMetadata().getName(),
+            event.getMetadata().getDescription(),
+            event.getCampaignId(),
+            event.getEventDate() != null ? event.getEventDate().toString() : "",
+            event.getCost() != null ? event.getCost().toString() : "",
+            event.getCurrency(),
+            event.getTimezone()
+        };
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        int offset = 0;
+        final int pageSize = 200;
+        while (true) {
+            Query query = new Query();
+            Condition condition = new Condition();
+            condition.setConditionType(definitionsService.getConditionType("matchAllCondition"));
+            query.setCondition(condition);
+            query.setOffset(offset);
+            query.setLimit(pageSize);
+
+            PartialList<CampaignEvent> events = goalsService.getEvents(query);
+            CampaignEvent event = events.getList().stream()
+                .filter(e -> e.getItemId().equals(id))
+                .findFirst()
+                .orElse(null);
+            if (event != null) {
+                return OBJECT_MAPPER.convertValue(event, Map.class);
+            }
+            if (offset + events.getList().size() >= events.getTotalSize()) {
+                return null;
+            }
+            offset += pageSize;
+        }
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        CampaignEvent event = OBJECT_MAPPER.convertValue(properties, CampaignEvent.class);
+        goalsService.setCampaignEvent(event);
+        return event.getItemId();
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        CampaignEvent updatedEvent = OBJECT_MAPPER.convertValue(properties, CampaignEvent.class);
+        updatedEvent.setItemId(id);
+        goalsService.setCampaignEvent(updatedEvent);
+    }
+
+    @Override
+    public void delete(String id) {
+        goalsService.removeCampaignEvent(id);
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return String.join("\n",
+            "Required properties:",
+            "- itemId: The unique identifier of the campaign event",
+            "- name: The name of the campaign event",
+            "- description: The description of the campaign event",
+            "- campaignId: The ID of the campaign this event belongs to",
+            "",
+            "Optional properties:",
+            "- scope: The scope of the campaign event (defaults to systemscope)",
+            "- eventDate: The date of the event (ISO-8601 format)",
+            "- cost: The cost associated with this event",
+            "- currency: The currency for the event cost",
+            "- timezone: The timezone for the event"
+        );
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java
new file mode 100644
index 000000000..b98560290
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java
@@ -0,0 +1,153 @@
+/*
+ * 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.unomi.shell.dev.commands.conditions;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.conditions.ConditionType;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class ConditionTypeCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+            "itemId", "scope", "name", "description", "conditionEvaluator", "queryBuilder", "parameters", "parentCondition"
+    );
+
+    @Override
+    public String getObjectType() {
+        return "conditiontype";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[] {
+            "Activated",
+            "Hidden",
+            "Read-only",
+            "Identifier",
+            "Scope",
+            "Name",
+            "Tags",
+            "System tags"
+        };
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        List<ConditionType> allTypes = new ArrayList<>(definitionsService.getAllConditionTypes());
+        int offset = query.getOffset();
+        int pageSize = query.getLimit();
+        int totalSize = allTypes.size();
+
+        List<ConditionType> pageTypes = allTypes.subList(
+            Math.min(offset, totalSize),
+            Math.min(offset + pageSize, totalSize)
+        );
+
+        return new PartialList<ConditionType>(pageTypes, offset, pageSize, totalSize, PartialList.Relation.EQUAL);
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        ConditionType type = (ConditionType) item;
+        Metadata metadata = type.getMetadata();
+        ArrayList<Comparable> rowData = new ArrayList<>();
+        rowData.add(metadata.isEnabled() ? "x" : "");
+        rowData.add(metadata.isHidden() ? "x" : "");
+        rowData.add(metadata.isReadOnly() ? "x" : "");
+        rowData.add(metadata.getId());
+        rowData.add(metadata.getScope());
+        rowData.add(metadata.getName());
+        rowData.add(StringUtils.join(metadata.getTags(), ","));
+        rowData.add(StringUtils.join(metadata.getSystemTags(), ","));
+        return rowData.toArray(new Comparable[0]);
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        ConditionType conditionType = OBJECT_MAPPER.convertValue(properties, ConditionType.class);
+        definitionsService.setConditionType(conditionType);
+        return conditionType.getItemId();
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        ConditionType conditionType = definitionsService.getConditionType(id);
+        if (conditionType == null) {
+            return null;
+        }
+        return OBJECT_MAPPER.convertValue(conditionType, Map.class);
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        ConditionType conditionType = definitionsService.getConditionType(id);
+        if (conditionType == null) {
+            throw new IllegalArgumentException("Condition type with id '" + id + "' not found");
+        }
+        ConditionType updatedConditionType = OBJECT_MAPPER.convertValue(properties, ConditionType.class);
+        updatedConditionType.setItemId(id);
+        definitionsService.setConditionType(updatedConditionType);
+    }
+
+    @Override
+    public void delete(String id) {
+        definitionsService.removeConditionType(id);
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return "Required properties:\n" +
+               "- itemId (string): Unique identifier for the condition type\n" +
+               "- scope (string): Scope of the condition type\n" +
+               "- name (string): Human-readable name\n" +
+               "\n" +
+               "Optional properties:\n" +
+               "- description (string): Description of the condition type\n" +
+               "- conditionEvaluator (string): Name of the condition evaluator implementation\n" +
+               "- queryBuilder (string): Name of the query builder implementation\n" +
+               "- parameters (array): List of parameters, each containing:\n" +
+               "  - id (string): Parameter identifier\n" +
+               "  - type (string): Parameter type\n" +
+               "  - multivalued (boolean): Whether the parameter accepts multiple values\n" +
+               "  - defaultValue (any): Default value for the parameter\n" +
+               "- parentCondition (object): Parent condition definition\n" +
+               "- enabled (boolean): Whether the condition type is enabled (default: true)\n" +
+               "- hidden (boolean): Whether the condition type is hidden (default: false)\n" +
+               "- readOnly (boolean): Whether the condition type is read-only (default: false)\n" +
+               "- tags (array): List of tags for the condition type\n" +
+               "- systemTags (array): List of system tags for the condition type";
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java
new file mode 100644
index 000000000..bb3d5c195
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java
@@ -0,0 +1,255 @@
+/*
+ * 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.unomi.shell.dev.commands.consents;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.Consent;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+/**
+ * A command to perform CRUD operations on consents
+ */
+@Component(service = CrudCommand.class, immediate = true)
+public class ConsentCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final String CONSENT_DATE_FORMAT_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
+    private static final ThreadLocal<DateFormat> DATE_FORMAT = ThreadLocal.withInitial(
+        () -> new SimpleDateFormat(CONSENT_DATE_FORMAT_PATTERN));
+    private static final List<String> PROPERTY_NAMES = List.of(
+        "profileId", "scope", "typeIdentifier", "status", "statusDate", "revokeDate"
+    );
+
+    @Reference
+    private ProfileService profileService;
+
+    @Override
+    public String getObjectType() {
+        return "consent";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[]{"Profile ID", "Scope", "Type", "Status", "Status Date", "Revoke Date"};
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        // Consents are embedded in profiles. We fetch consentLimit*20 profiles from offset 0,
+        // then paginate the extracted consent list with the original query parameters.
+        // LIMITATION: always starts from profile offset 0, so consents on profiles outside
+        // the first consentLimit*20 results may not appear for high-offset pages.
+        int consentLimit = query.getLimit() > 0 ? query.getLimit() : 100;
+        Query profileQuery = new Query();
+        profileQuery.setLimit(consentLimit * 20);
+        profileQuery.setOffset(0);
+        profileQuery.setSortby(query.getSortby());
+        profileQuery.setCondition(query.getCondition());
+        PartialList<Profile> profiles = profileService.search(profileQuery, Profile.class);
+        List<Map<String, Object>> consents = new ArrayList<>();
+
+        DateFormat dateFormat = DATE_FORMAT.get();
+        for (Profile profile : profiles.getList()) {
+            Map<String, Object> profileProperties = profile.getProperties();
+            if (profileProperties.containsKey("consents")) {
+                @SuppressWarnings("unchecked")
+                Map<String, Consent> profileConsents = (Map<String, Consent>) profileProperties.get("consents");
+                for (Map.Entry<String, Consent> entry : profileConsents.entrySet()) {
+                    Map<String, Object> consentMap = entry.getValue().toMap(dateFormat);
+                    consentMap.put("profileId", profile.getItemId());
+                    consents.add(consentMap);
+                }
+            }
+        }
+
+        return paginateList(consents, query);
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        @SuppressWarnings("unchecked")
+        Map<String, Object> consentMap = (Map<String, Object>) item;
+        return new Comparable[]{
+            String.valueOf(consentMap.get("profileId")),
+            String.valueOf(consentMap.get("scope")),
+            String.valueOf(consentMap.get("typeIdentifier")),
+            String.valueOf(consentMap.get("status")),
+            String.valueOf(consentMap.get("statusDate")),
+            String.valueOf(consentMap.get("revokeDate"))
+        };
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        // The ID format is expected to be "profileId:typeIdentifier"
+        String[] parts = id.split(":");
+        if (parts.length != 2) {
+            return null;
+        }
+
+        String profileId = parts[0];
+        String typeIdentifier = parts[1];
+
+        Profile profile = profileService.load(profileId);
+        if (profile == null) {
+            return null;
+        }
+
+        Map<String, Object> profileProperties = profile.getProperties();
+        if (!profileProperties.containsKey("consents")) {
+            return null;
+        }
+
+        @SuppressWarnings("unchecked")
+        Map<String, Consent> consents = (Map<String, Consent>) profileProperties.get("consents");
+        Consent consent = consents.get(typeIdentifier);
+
+        if (consent == null) {
+            return null;
+        }
+
+        Map<String, Object> consentMap = consent.toMap(DATE_FORMAT.get());
+        consentMap.put("profileId", profileId);
+        return consentMap;
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        String profileId = (String) properties.remove("profileId");
+        if (profileId == null) {
+            return null;
+        }
+
+        Profile profile = profileService.load(profileId);
+        if (profile == null) {
+            return null;
+        }
+
+        try {
+            Consent consent = new Consent(properties, DATE_FORMAT.get());
+
+            Map<String, Object> profileProperties = profile.getProperties();
+            @SuppressWarnings("unchecked")
+            Map<String, Consent> consents = profileProperties.containsKey("consents") ?
+                (Map<String, Consent>) profileProperties.get("consents") : new HashMap<>();
+
+            consents.put(consent.getTypeIdentifier(), consent);
+            profileProperties.put("consents", consents);
+            profile.setProperties(profileProperties);
+
+            profileService.save(profile);
+            return profileId + ":" + consent.getTypeIdentifier();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        // The ID format is expected to be "profileId:typeIdentifier"
+        String[] parts = id.split(":");
+        if (parts.length != 2) {
+            return;
+        }
+
+        String profileId = parts[0];
+        String typeIdentifier = parts[1];
+
+        Profile profile = profileService.load(profileId);
+        if (profile == null) {
+            return;
+        }
+
+        try {
+            Consent consent = new Consent(properties, DATE_FORMAT.get());
+
+            Map<String, Object> profileProperties = profile.getProperties();
+            @SuppressWarnings("unchecked")
+            Map<String, Consent> consents = profileProperties.containsKey("consents") ?
+                (Map<String, Consent>) profileProperties.get("consents") : new HashMap<>();
+
+            consents.put(typeIdentifier, consent);
+            profileProperties.put("consents", consents);
+            profile.setProperties(profileProperties);
+
+            profileService.save(profile);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("Failed to update consent: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public void delete(String id) {
+        // The ID format is expected to be "profileId:typeIdentifier"
+        String[] parts = id.split(":");
+        if (parts.length != 2) {
+            return;
+        }
+
+        String profileId = parts[0];
+        String typeIdentifier = parts[1];
+
+        Profile profile = profileService.load(profileId);
+        if (profile == null) {
+            return;
+        }
+
+        Map<String, Object> profileProperties = profile.getProperties();
+        if (profileProperties.containsKey("consents")) {
+            @SuppressWarnings("unchecked")
+            Map<String, Consent> consents = (Map<String, Consent>) profileProperties.get("consents");
+            consents.remove(typeIdentifier);
+            profileProperties.put("consents", consents);
+            profile.setProperties(profileProperties);
+            profileService.save(profile);
+        }
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return String.join("\n",
+            "Required properties:",
+            "- profileId: The identifier of the profile this consent belongs to",
+            "- typeIdentifier: The unique identifier for the consent type",
+            "- scope: The scope for this consent",
+            "- status: The consent status (GRANTED, DENIED, or REVOKED)",
+            "",
+            "Optional properties:",
+            "- statusDate: The date from which this consent applies (ISO-8601 format)",
+            "- revokeDate: The date at which this consent will expire (ISO-8601 format)"
+        );
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java
new file mode 100644
index 000000000..697867bd3
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java
@@ -0,0 +1,167 @@
+/*
+ * 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.unomi.shell.dev.commands.events;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.Event;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class EventCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+        "itemId", "eventType", "scope", "source", "target", "properties", "persistent"
+    );
+    private static final List<String> EVENT_TYPES = List.of(
+        "view", "form", "login", "sessionCreated", "sessionReassigned", "profileUpdated", "incrementTrait",
+        "modifyConsent", "updateProperties", "identify", "impersonate", "matching"
+    );
+
+    @Reference
+    private EventService eventService;
+
+    @Override
+    public String getObjectType() {
+        return "event";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[] {
+            "Identifier",
+            "Event Type",
+            "Scope",
+            "Source ID",
+            "Target ID",
+            "Profile ID",
+            "Session ID",
+            "Timestamp"
+        };
+    }
+
+    @Override
+    protected String getSortBy() {
+        return "timeStamp:desc";
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        return eventService.search(query);
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        Event event = (Event) item;
+        ArrayList<Comparable> rowData = new ArrayList<>();
+        rowData.add(event.getItemId());
+        rowData.add(event.getEventType());
+        rowData.add(event.getScope());
+        rowData.add(event.getSource() != null ? event.getSource().getItemId() : "");
+        rowData.add(event.getTarget() != null ? event.getTarget().getItemId() : "");
+        rowData.add(event.getProfileId());
+        rowData.add(event.getSessionId());
+        rowData.add(event.getTimeStamp());
+        return rowData.toArray(new Comparable[0]);
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        Event event = OBJECT_MAPPER.convertValue(properties, Event.class);
+        eventService.send(event);
+        return event.getItemId();
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        Event event = eventService.getEvent(id);
+        if (event == null) {
+            return null;
+        }
+        return OBJECT_MAPPER.convertValue(event, Map.class);
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        properties.put("itemId", id);
+        Event event = OBJECT_MAPPER.convertValue(properties, Event.class);
+        eventService.send(event);
+    }
+
+    @Override
+    public void delete(String id) {
+        eventService.deleteEvent(id);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return String.join("\n",
+            "Required properties:",
+            "- itemId: Event ID (string)",
+            "- eventType: Type of event",
+            "- scope: Event scope",
+            "- source: Source object (Item)",
+            "- target: Target object (Item)",
+            "",
+            "Optional properties:",
+            "- properties: Map of event properties",
+            "- persistent: Whether event should be persisted (boolean)",
+            "",
+            "Common event types:",
+            "- view: Page/content view",
+            "- form: Form submission",
+            "- login: User login",
+            "- sessionCreated: New session",
+            "- sessionReassigned: Session reassigned",
+            "- profileUpdated: Profile update",
+            "- incrementTrait: Increment profile trait",
+            "- modifyConsent: Consent change",
+            "- updateProperties: Property update",
+            "- identify: Profile identification",
+            "- impersonate: Profile impersonation",
+            "- matching: Profile matching"
+        );
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public List<String> completePropertyValue(String propertyName, String prefix) {
+        if ("eventType".equals(propertyName)) {
+            return EVENT_TYPES.stream()
+                    .filter(type -> type.startsWith(prefix))
+                    .collect(Collectors.toList());
+        }
+        return List.of();
+    }
+
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java
new file mode 100644
index 000000000..7f03aba9b
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java
@@ -0,0 +1,143 @@
+/*
+ * 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.unomi.shell.dev.commands.goals;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.goals.Goal;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.GoalsService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * A command to perform CRUD operations on goals
+ */
+@Component(service = CrudCommand.class, immediate = true)
+public class GoalCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+        "itemId", "name", "description", "scope", "campaignId", "enabled"
+    );
+
+    @Reference
+    private GoalsService goalsService;
+
+    @Override
+    public String getObjectType() {
+        return "goal";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[]{"ID", "Name", "Description", "Scope", "Campaign ID", "Enabled"};
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        // Convert Set<Metadata> to PartialList for consistency
+        Set<Metadata> metadatas = goalsService.getGoalMetadatas(query);
+        List<Goal> goals = metadatas.stream()
+            .map(metadata -> goalsService.getGoal(metadata.getId()))
+            .filter(goal -> goal != null)
+            .collect(Collectors.toList());
+        // getGoalMetadatas returns Set<Metadata> with no totalSize information; we cannot distinguish
+        // "hit limit" from "exactly that many items exist", so we report goals.size() as totalSize.
+        return new PartialList<>(goals, query.getOffset(), goals.size(), goals.size(), PartialList.Relation.EQUAL);
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        Goal goal = (Goal) item;
+        return new Comparable[]{
+            goal.getItemId(),
+            goal.getMetadata().getName(),
+            goal.getMetadata().getDescription(),
+            goal.getMetadata().getScope(),
+            goal.getCampaignId(),
+            goal.getMetadata().isEnabled()
+        };
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        Goal goal = goalsService.getGoal(id);
+        if (goal == null) {
+            return null;
+        }
+        return OBJECT_MAPPER.convertValue(goal, Map.class);
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        Goal goal = OBJECT_MAPPER.convertValue(properties, Goal.class);
+        goalsService.setGoal(goal);
+        return goal.getItemId();
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        Goal existingGoal = goalsService.getGoal(id);
+        if (existingGoal == null) {
+            throw new IllegalArgumentException("Goal not found with ID: " + id);
+        }
+
+        Goal updatedGoal = OBJECT_MAPPER.convertValue(properties, Goal.class);
+        updatedGoal.setItemId(id);
+        goalsService.setGoal(updatedGoal);
+    }
+
+    @Override
+    public void delete(String id) {
+        Goal goal = goalsService.getGoal(id);
+        if (goal != null) {
+            goalsService.removeGoal(id);
+        }
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return String.join("\n",
+            "Required properties:",
+            "- itemId: The unique identifier of the goal",
+            "- name: The name of the goal",
+            "- description: The description of the goal",
+            "",
+            "Optional properties:",
+            "- scope: The scope of the goal (defaults to systemscope)",
+            "- campaignId: The ID of the associated campaign",
+            "- enabled: Whether the goal is enabled (true/false)",
+            "- startEvent: The condition that triggers the start of the goal",
+            "- targetEvent: The condition that marks the goal as achieved"
+        );
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java
new file mode 100644
index 000000000..5633e89c4
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java
@@ -0,0 +1,153 @@
+/*
+ * 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.unomi.shell.dev.commands.personas;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Persona;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class PersonaCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = Arrays.asList(
+            "firstName",
+            "lastName",
+            "email",
+            "description",
+            "properties"
+    );
+
+    @Reference
+    private ProfileService profileService;
+
+    @Override
+    public String getObjectType() {
+        return Persona.ITEM_TYPE;
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[] {
+            "Identifier",
+            "First Name",
+            "Last Name",
+            "Email",
+            "Description",
+            "Last Updated"
+        };
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        return profileService.search(query, Persona.class);
+    }
+
+    @Override
+    protected String[] buildRow(Object item) {
+        Persona persona = (Persona) item;
+        Object lastUpdated = persona.getSystemProperties() != null
+            ? persona.getSystemProperties().get("lastUpdated")
+            : null;
+        return new String[] {
+            persona.getItemId(),
+            (String) persona.getProperty("firstName"),
+            (String) persona.getProperty("lastName"),
+            (String) persona.getProperty("email"),
+            (String) persona.getProperty("description"),
+            lastUpdated != null ? lastUpdated.toString() : ""
+        };
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        String personaId = (String) properties.get("itemId");
+        if (personaId == null) {
+            throw new IllegalArgumentException("itemId is required");
+        }
+
+        Persona persona = new Persona(personaId);
+        for (Map.Entry<String, Object> entry : properties.entrySet()) {
+            if (!entry.getKey().equals("itemId")) {
+                persona.setProperty(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Persona saved = profileService.savePersona(persona);
+        return saved.getItemId();
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        Persona persona = profileService.loadPersona(id);
+        return persona != null ? OBJECT_MAPPER.convertValue(persona, Map.class) : null;
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        Persona persona = profileService.loadPersona(id);
+        if (persona == null) {
+            throw new IllegalArgumentException("Persona not found: " + id);
+        }
+
+        for (Map.Entry<String, Object> entry : properties.entrySet()) {
+            persona.setProperty(entry.getKey(), entry.getValue());
+        }
+
+        profileService.savePersona(persona);
+    }
+
+    @Override
+    public void delete(String id) {
+        profileService.delete(id, true);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return "Required properties:\n" +
+               "- itemId: The identifier for the persona\n" +
+               "\n" +
+               "Optional properties:\n" +
+               "- firstName: The persona's first name\n" +
+               "- lastName: The persona's last name\n" +
+               "- email: The persona's email address\n" +
+               "- description: A description of what this persona represents\n" +
+               "- properties: Additional properties as a JSON object";
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public List<String> completePropertyValue(String propertyName, String prefix) {
+        return List.of();
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java
new file mode 100644
index 000000000..fb5f38391
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java
@@ -0,0 +1,159 @@
+/*
+ * 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.unomi.shell.dev.commands.profiles;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.ProfileAlias;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A command to perform CRUD operations on profile aliases
+ */
+@Component(service = CrudCommand.class, immediate = true)
+public class ProfileAliasCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+        "itemId", "profileID", "clientID", "creationTime", "modifiedTime"
+    );
+
+    @Reference
+    private ProfileService profileService;
+
+    @Override
+    public String getObjectType() {
+        return "profilealias";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[]{"ID", "Profile ID", "Client ID", "Created", "Modified"};
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        // Since there's no direct method to search all aliases, we'll use findProfileAliases with null profileId
+        return profileService.findProfileAliases(null, query.getOffset(), query.getLimit(), query.getSortby());
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        ProfileAlias alias = (ProfileAlias) item;
+        return new Comparable[]{
+            alias.getItemId(),
+            alias.getProfileID(),
+            alias.getClientID(),
+            alias.getCreationTime() != null ? alias.getCreationTime().toString() : "",
+            alias.getModifiedTime() != null ? alias.getModifiedTime().toString() : ""
+        };
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        int offset = 0;
+        final int pageSize = 200;
+        while (true) {
+            PartialList<ProfileAlias> page = profileService.findProfileAliases(null, offset, pageSize, null);
+            ProfileAlias alias = page.getList().stream()
+                .filter(a -> a.getItemId().equals(id))
+                .findFirst()
+                .orElse(null);
+            if (alias != null) {
+                return OBJECT_MAPPER.convertValue(alias, Map.class);
+            }
+            if (offset + page.getList().size() >= page.getTotalSize()) {
+                return null;
+            }
+            offset += pageSize;
+        }
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        ProfileAlias alias = OBJECT_MAPPER.convertValue(properties, ProfileAlias.class);
+        // Set creation and modification time if not provided
+        if (alias.getCreationTime() == null) {
+            alias.setCreationTime(new Date());
+        }
+        if (alias.getModifiedTime() == null) {
+            alias.setModifiedTime(alias.getCreationTime());
+        }
+
+        // Add the alias to the profile
+        profileService.addAliasToProfile(alias.getProfileID(), alias.getItemId(), alias.getClientID());
+        return alias.getItemId();
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        // Get the existing alias (check if it exists and get its data in one call)
+        Map<String, Object> aliasData = read(id);
+        if (aliasData == null) {
+            return;
+        }
+
+        // Remove the old alias and add the new one
+        ProfileAlias oldAlias = OBJECT_MAPPER.convertValue(aliasData, ProfileAlias.class);
+        profileService.removeAliasFromProfile(oldAlias.getProfileID(), oldAlias.getItemId(), oldAlias.getClientID());
+
+        ProfileAlias updatedAlias = OBJECT_MAPPER.convertValue(properties, ProfileAlias.class);
+        updatedAlias.setItemId(id);
+        updatedAlias.setModifiedTime(new Date());
+        profileService.addAliasToProfile(updatedAlias.getProfileID(), updatedAlias.getItemId(), updatedAlias.getClientID());
+    }
+
+    @Override
+    public void delete(String id) {
+        // First check if the alias exists
+        Map<String, Object> aliasData = read(id);
+        if (aliasData != null) {
+            ProfileAlias alias = OBJECT_MAPPER.convertValue(aliasData, ProfileAlias.class);
+            profileService.removeAliasFromProfile(alias.getProfileID(), alias.getItemId(), alias.getClientID());
+        }
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return String.join("\n",
+            "Required properties:",
+            "- itemId: The unique identifier of the alias",
+            "- profileID: The identifier of the profile this alias belongs to",
+            "- clientID: The identifier of the client that created this alias",
+            "",
+            "Optional properties:",
+            "- creationTime: The creation timestamp (ISO-8601 format)",
+            "- modifiedTime: The last modification timestamp (ISO-8601 format)"
+        );
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java
new file mode 100644
index 000000000..1ac66997b
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java
@@ -0,0 +1,186 @@
+/*
+ * 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.unomi.shell.dev.commands.profiles;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.apache.unomi.api.conditions.Condition;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class ProfileCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = Arrays.asList(
+        "itemId",
+        "properties.firstName",
+        "properties.lastName",
+        "properties.email",
+        "segments",
+        "scores",
+        "consents",
+        "systemProperties.lastUpdated"
+    );
+
+    private static final String[] TABLE_HEADERS = {
+        "Identifier",
+        "First Name",
+        "Last Name",
+        "Email",
+        "Segments",
+        "Last Updated",
+        "Scope"
+    };
+
+    @Reference
+    private ProfileService profileService;
+
+    @Override
+    public String getObjectType() {
+        return "profile";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return TABLE_HEADERS;
+    }
+
+    @Override
+    protected String getSortBy() {
+        return "systemProperties.lastUpdated:desc,properties.lastVisit:desc";
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        return profileService.search(query, Profile.class);
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        Profile profile = (Profile) item;
+        Map<String, Object> systemProperties = profile.getSystemProperties();
+        return new Comparable[] {
+            profile.getItemId(),
+            (String) profile.getProperty("firstName"),
+            (String) profile.getProperty("lastName"),
+            (String) profile.getProperty("email"),
+            profile.getSegments().size(),
+            systemProperties != null ? (Comparable) systemProperties.get("lastUpdated") : null,
+            profile.getScope()
+        };
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        String itemId = (String) properties.get("itemId");
+        Profile profile = itemId != null ? new Profile(itemId) : new Profile();
+        Map<String, Object> profileProps = new java.util.HashMap<>(properties);
+        profileProps.remove("itemId");
+        profile.setProperties(profileProps);
+        profileService.save(profile);
+        return profile.getItemId();
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        Profile profile = profileService.load(id);
+        return profile != null ? OBJECT_MAPPER.convertValue(profile, Map.class) : null;
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        Profile profile = profileService.load(id);
+        if (profile == null) {
+            throw new IllegalArgumentException("Profile not found with ID: " + id);
+        }
+        profile.setProperties(properties);
+        profileService.save(profile);
+    }
+
+    @Override
+    public void delete(String id) {
+        profileService.delete(id, false);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return "Required properties:\n" +
+               "  - itemId: Unique identifier for the profile\n" +
+               "\n" +
+               "Optional properties:\n" +
+               "  - properties.firstName: First name\n" +
+               "  - properties.lastName: Last name\n" +
+               "  - properties.email: Email address\n" +
+               "  - properties.phoneNumber: Phone number\n" +
+               "  - properties.address: Address\n" +
+               "  - properties.company: Company name\n" +
+               "  - properties.jobTitle: Job title\n" +
+               "  - consents: Map of consent IDs to consent status\n" +
+               "  - scores: Map of scoring plan IDs to scores";
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public List<String> completeId(String prefix) {
+        // Create a query with filter by ID prefix if provided
+        Query query = new Query();
+        query.setLimit(20); // Reasonable limit for auto-completion
+
+        try {
+            // If prefix is not empty, filter by it
+            if (!prefix.isEmpty()) {
+                Condition condition = new Condition(definitionsService.getConditionType("profilePropertyCondition"));
+                condition.setParameter("propertyName", "itemId");
+                condition.setParameter("comparisonOperator", "startsWith");
+                condition.setParameter("propertyValue", prefix);
+                query.setCondition(condition);
+            } else {
+                // Otherwise, match all
+                Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition"));
+                query.setCondition(matchAllCondition);
+            }
+
+            query.setSortby("properties.lastVisit:desc"); // Sort by last visit to show most recent profiles first
+
+            // Execute the query
+            PartialList<Profile> profiles = profileService.search(query, Profile.class);
+
+            // Extract IDs
+            return profiles.getList().stream()
+                .map(Profile::getItemId)
+                .collect(Collectors.toList());
+        } catch (Exception e) {
+            return List.of(); // Return empty list on error
+        }
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java
new file mode 100644
index 000000000..55b31301f
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java
@@ -0,0 +1,174 @@
+/*
+ * 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.unomi.shell.dev.commands.properties;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.PropertyType;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class PropertyTypeCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+        "itemId", "name", "description", "scope", "tags", "systemTags", "target", "valueTypeId",
+        "defaultValue", "rank", "mergeStrategy", "multivalued", "protected"
+    );
+
+    @Reference
+    private ProfileService profileService;
+
+    @Override
+    public String getObjectType() {
+        return "propertytype";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[] {
+            "Identifier",
+            "Name",
+            "Description",
+            "Target",
+            "Value Type",
+            "Default Value",
+            "Multivalued",
+            "Protected",
+            "Rank",
+            "Merge Strategy",
+            "Scope",
+            "Tags",
+            "System Tags"
+        };
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        Map<String, Collection<PropertyType>> propertyTypesByTarget = profileService.getTargetPropertyTypes();
+        List<PropertyType> propertyTypes = new ArrayList<>();
+        
+        // Combine all property types from all targets
+        for (Collection<PropertyType> typeCollection : propertyTypesByTarget.values()) {
+            propertyTypes.addAll(typeCollection);
+        }
+        
+        return paginateList(propertyTypes, query);
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        PropertyType propertyType = (PropertyType) item;
+        return new Comparable[] {
+            propertyType.getItemId(),
+            propertyType.getMetadata().getName(),
+            propertyType.getMetadata().getDescription(),
+            propertyType.getTarget(),
+            propertyType.getValueTypeId(),
+            propertyType.getDefaultValue(),
+            String.valueOf(propertyType.isMultivalued()),
+            String.valueOf(propertyType.isProtected()),
+            propertyType.getRank() != null ? propertyType.getRank().toString() : "",
+            propertyType.getMergeStrategy(),
+            propertyType.getMetadata().getScope(),
+            String.join(",", propertyType.getMetadata().getTags()),
+            String.join(",", propertyType.getMetadata().getSystemTags())
+        };
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        PropertyType propertyType = OBJECT_MAPPER.convertValue(properties, PropertyType.class);
+        profileService.setPropertyType(propertyType);
+        return propertyType.getItemId();
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        PropertyType propertyType = profileService.getPropertyType(id);
+        if (propertyType != null) {
+            return OBJECT_MAPPER.convertValue(propertyType, Map.class);
+        }
+        return null;
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        PropertyType propertyType = OBJECT_MAPPER.convertValue(properties, PropertyType.class);
+        propertyType.setItemId(id);
+        profileService.setPropertyType(propertyType);
+    }
+
+    @Override
+    public void delete(String id) {
+        profileService.deletePropertyType(id);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return String.join("\n",
+            "Required properties:",
+            "- itemId: Identifier for the property type",
+            "- name: Name of the property type",
+            "- valueTypeId: Type of value (string, integer, long, date, etc.)",
+            "",
+            "Optional properties:",
+            "- description: Description of the property type",
+            "- target: Target type (e.g., 'profiles')",
+            "- defaultValue: Default value for the property",
+            "- rank: Numeric rank for ordering",
+            "- mergeStrategy: Strategy for merging property values",
+            "- multivalued: Whether the property can have multiple values (true/false)",
+            "- protected: Whether the property is read-only (true/false)",
+            "- scope: Scope of the property type",
+            "- tags: List of tags",
+            "- systemTags: List of system tags"
+        );
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public List<String> completePropertyValue(String propertyName, String prefix) {
+        if ("valueTypeId".equals(propertyName)) {
+            return List.of("string", "integer", "long", "date", "boolean", "json")
+                    .stream()
+                    .filter(type -> type.startsWith(prefix))
+                    .collect(Collectors.toList());
+        }
+        return List.of();
+    }
+
+    public void setProfileService(ProfileService profileService) {
+        this.profileService = profileService;
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java
new file mode 100644
index 000000000..f99e5ad94
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java
@@ -0,0 +1,247 @@
+/*
+ * 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.unomi.shell.dev.commands.rules;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.query.Query;
+import org.apache.karaf.shell.support.table.ShellTable;
+import org.apache.unomi.api.rules.Rule;
+import org.apache.unomi.api.rules.RuleStatistics;
+import org.apache.unomi.api.services.RulesService;
+import org.apache.unomi.common.DataTable;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.apache.unomi.api.conditions.Condition;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class RuleCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+        "itemId", "name", "description", "priority", "condition", "actions", "metadata"
+    );
+    private static final List<String> CONDITION_TYPES = List.of(
+        "booleanCondition", "profilePropertyCondition", "sessionPropertyCondition", "eventPropertyCondition",
+        "pastEventCondition", "matchAllCondition", "notCondition", "orCondition", "andCondition"
+    );
+
+    @Reference
+    private RulesService rulesService;
+
+    /** Cached for the duration of a single list/query operation (see {@link #buildQuery}). */
+    private Map<String, RuleStatistics> statisticsSnapshot;
+
+    @Override
+    public String getObjectType() {
+        return "rule";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[] {
+            "Activated",
+            "Hidden",
+            "Read-only",
+            "Identifier",
+            "Scope",
+            "Name",
+            "Tags",
+            "System tags",
+            "Executions",
+            "Conditions [ms]",
+            "Actions [ms]"
+        };
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        return rulesService.getRuleDetails(query);
+    }
+
+    @Override
+    protected DataTable buildDataTable() {
+        try {
+            statisticsSnapshot = rulesService.getAllRuleStatistics();
+            return super.buildDataTable();
+        } finally {
+            statisticsSnapshot = null;
+        }
+    }
+
+    @Override
+    public void buildRows(ShellTable table, int maxEntries) {
+        try {
+            statisticsSnapshot = rulesService.getAllRuleStatistics();
+            super.buildRows(table, maxEntries);
+        } finally {
+            statisticsSnapshot = null;
+        }
+    }
+
+    @Override
+    public void buildCsvOutput(PrintStream console, String[] headers, int limit) throws Exception {
+        try {
+            statisticsSnapshot = rulesService.getAllRuleStatistics();
+            super.buildCsvOutput(console, headers, limit);
+        } finally {
+            statisticsSnapshot = null;
+        }
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        Rule rule = (Rule) item;
+        String ruleId = rule.getItemId();
+        Map<String, RuleStatistics> allRuleStatistics = statisticsSnapshot != null
+            ? statisticsSnapshot
+            : rulesService.getAllRuleStatistics();
+
+        ArrayList<Comparable> rowData = new ArrayList<>();
+        rowData.add(rule.getMetadata().isEnabled() ? "x" : "");
+        rowData.add(rule.getMetadata().isHidden() ? "x" : "");
+        rowData.add(rule.getMetadata().isReadOnly() ? "x" : "");
+        rowData.add(ruleId);
+        rowData.add(rule.getMetadata().getScope());
+        rowData.add(rule.getMetadata().getName());
+        rowData.add(StringUtils.join(rule.getMetadata().getTags(), ","));
+        rowData.add(StringUtils.join(rule.getMetadata().getSystemTags(), ","));
+        RuleStatistics ruleStatistics = allRuleStatistics.get(ruleId);
+        if (ruleStatistics != null) {
+            rowData.add(ruleStatistics.getExecutionCount());
+            rowData.add(ruleStatistics.getConditionsTime());
+            rowData.add(ruleStatistics.getActionsTime());
+        } else {
+            rowData.add(0L);
+            rowData.add(0L);
+            rowData.add(0L);
+        }
+        return rowData.toArray(new Comparable[0]);
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        Rule rule = OBJECT_MAPPER.convertValue(properties, Rule.class);
+        rulesService.setRule(rule);
+        return rule.getItemId();
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        Rule rule = rulesService.getRule(id);
+        if (rule == null) {
+            return null;
+        }
+        return OBJECT_MAPPER.convertValue(rule, Map.class);
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        properties.put("itemId", id);
+        Rule rule = OBJECT_MAPPER.convertValue(properties, Rule.class);
+        rulesService.setRule(rule);
+    }
+
+    @Override
+    public void delete(String id) {
+        rulesService.removeRule(id);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return String.join("\n",
+            "Required properties:",
+            "- itemId: Rule ID (string)",
+            "- name: Rule name",
+            "- condition: Rule condition object",
+            "- actions: List of rule actions",
+            "",
+            "Optional properties:",
+            "- description: Rule description",
+            "- priority: Rule priority (integer)",
+            "- metadata: Rule metadata",
+            "",
+            "Condition types:",
+            "- booleanCondition: Simple true/false condition",
+            "- profilePropertyCondition: Match profile property",
+            "- sessionPropertyCondition: Match session property",
+            "- eventPropertyCondition: Match event property",
+            "- pastEventCondition: Match past events",
+            "- matchAllCondition: Match all sub-conditions",
+            "- notCondition: Negate sub-condition",
+            "- orCondition: Match any sub-condition",
+            "- andCondition: Match all sub-conditions"
+        );
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public List<String> completePropertyValue(String propertyName, String prefix) {
+        if ("condition.type".equals(propertyName)) {
+            return CONDITION_TYPES.stream()
+                    .filter(type -> type.startsWith(prefix))
+                    .collect(Collectors.toList());
+        }
+        return List.of();
+    }
+
+    @Override
+    public List<String> completeId(String prefix) {
+        // Create a query to find rules that match the prefix
+        Query query = new Query();
+        query.setLimit(20); // Reasonable limit for auto-completion
+        
+        // If prefix is not empty, use it to filter results
+        if (!prefix.isEmpty()) {
+            Condition condition = new Condition(definitionsService.getConditionType("sessionPropertyCondition"));
+            condition.setParameter("comparisonOperator", "startsWith");
+            condition.setParameter("propertyName", "itemId");
+            condition.setParameter("propertyValue", prefix);
+            query.setCondition(condition);
+        } else {
+            // Otherwise, match all
+            Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition"));
+            query.setCondition(matchAllCondition);
+        }
+        
+        // Execute the query and extract rule IDs
+        try {
+            PartialList<Metadata> metadatas = rulesService.getRuleMetadatas(query);
+            return metadatas.getList().stream()
+                .map(Metadata::getId)
+                .collect(Collectors.toList());
+        } catch (Exception e) {
+            return List.of(); // Return empty list on error
+        }
+    }
+
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java
new file mode 100644
index 000000000..84baa0e08
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java
@@ -0,0 +1,133 @@
+/*
+ * 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.unomi.shell.dev.commands.rules;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.rules.RuleStatistics;
+import org.apache.unomi.api.services.RulesService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A command to perform CRUD operations on rule statistics
+ */
+@Component(service = CrudCommand.class, immediate = true)
+public class RuleStatisticsCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+        "itemId", "executionCount", "localExecutionCount", "conditionsTime", "localConditionsTime", "actionsTime", "localActionsTime", "lastSyncDate"
+    );
+
+    @Reference
+    private RulesService rulesService;
+
+    @Override
+    public String getObjectType() {
+        return "rulestats";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[]{"ID", "Executions", "Local Executions", "Conditions Time", "Local Conditions Time", "Actions Time", "Local Actions Time", "Last Sync"};
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        // Get all rules and their statistics
+        Map<String, RuleStatistics> statisticsMap = rulesService.getAllRuleStatistics();
+        List<RuleStatistics> statistics = new ArrayList<>(statisticsMap.values());
+        return new PartialList<>(statistics, 0, statistics.size(), statistics.size(), PartialList.Relation.EQUAL);
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        RuleStatistics stats = (RuleStatistics) item;
+        return new Comparable[]{
+            stats.getItemId(),
+            stats.getExecutionCount(),
+            stats.getLocalExecutionCount(),
+            stats.getConditionsTime(),
+            stats.getLocalConditionsTime(),
+            stats.getActionsTime(),
+            stats.getLocalActionsTime(),
+            stats.getLastSyncDate() != null ? stats.getLastSyncDate().toString() : ""
+        };
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        RuleStatistics stats = rulesService.getRuleStatistics(id);
+        if (stats == null) {
+            return null;
+        }
+        return OBJECT_MAPPER.convertValue(stats, Map.class);
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        throw new UnsupportedOperationException(
+            "Rule statistics are managed automatically by the rules engine and cannot be created via the shell.");
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        throw new UnsupportedOperationException(
+            "Rule statistics are managed automatically by the rules engine and cannot be updated via the shell.");
+    }
+
+    @Override
+    public void delete(String id) {
+        throw new UnsupportedOperationException(
+            "Rule statistics cannot be deleted individually via the shell (requested id: " + id
+                + "). Statistics are maintained by the rules engine.");
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return String.join("\n",
+            "Rule statistics are automatically managed by the rules engine and cannot be directly modified.",
+            "You can view statistics using the following properties:",
+            "",
+            "- itemId: The unique identifier of the rule statistics (matches the rule ID)",
+            "- executionCount: Total number of rule executions in the cluster",
+            "- localExecutionCount: Number of rule executions on this node since last sync",
+            "- conditionsTime: Total time spent evaluating conditions in the cluster (ms)",
+            "- localConditionsTime: Time spent evaluating conditions on this node since last sync (ms)",
+            "- actionsTime: Total time spent executing actions in the cluster (ms)",
+            "- localActionsTime: Time spent executing actions on this node since last sync (ms)",
+            "- lastSyncDate: Date of the last synchronization with the cluster",
+            "",
+            "Supported operations: list, read, help only (create, update, and delete are not supported)."
+        );
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java
new file mode 100644
index 000000000..10bef1fca
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java
@@ -0,0 +1,139 @@
+/*
+ * 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.unomi.shell.dev.commands.scopes;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Scope;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ScopeService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.persistence.spi.PersistenceService;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class ScopeCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+        "itemId", "name", "description", "restrictedVisibility", "metadata"
+    );
+
+    @Reference
+    private ScopeService scopeService;
+
+    @Reference
+    private PersistenceService persistenceService;
+
+    @Override
+    public String getObjectType() {
+        return "scope";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[] {
+            "Activated",
+            "Hidden",
+            "Read-only",
+            "Identifier",
+            "Name",
+            "Description",
+            "Restricted",
+            "Tags",
+            "System tags"
+        };
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        return persistenceService.query(query.getCondition(), query.getSortby(), Scope.class, query.getOffset(), query.getLimit());
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        Scope scope = (Scope) item;
+        ArrayList<Comparable> rowData = new ArrayList<>();
+        rowData.add(scope.getMetadata().isEnabled() ? "x" : "");
+        rowData.add(scope.getMetadata().isHidden() ? "x" : "");
+        rowData.add(scope.getMetadata().isReadOnly() ? "x" : "");
+        rowData.add(scope.getItemId());
+        rowData.add(scope.getMetadata().getName());
+        rowData.add(scope.getMetadata().getDescription());
+        rowData.add(""); // No restricted visibility in Scope class
+        rowData.add(StringUtils.join(scope.getMetadata().getTags(), ","));
+        rowData.add(StringUtils.join(scope.getMetadata().getSystemTags(), ","));
+        return rowData.toArray(new Comparable[0]);
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        Scope scope = OBJECT_MAPPER.convertValue(properties, Scope.class);
+        scopeService.save(scope);
+        return scope.getItemId();
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        Scope scope = scopeService.getScope(id);
+        if (scope == null) {
+            return null;
+        }
+        return OBJECT_MAPPER.convertValue(scope, Map.class);
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        properties.put("itemId", id);
+        Scope scope = OBJECT_MAPPER.convertValue(properties, Scope.class);
+        scopeService.save(scope);
+    }
+
+    @Override
+    public void delete(String id) {
+        scopeService.delete(id);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return String.join("\n",
+            "Required properties:",
+            "- itemId: Scope ID (string)",
+            "- name: Scope name",
+            "",
+            "Optional properties:",
+            "- description: Scope description",
+            "- restrictedVisibility: Whether scope has restricted visibility (boolean)",
+            "- metadata: Scope metadata"
+        );
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java
new file mode 100644
index 000000000..78fd511dd
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java
@@ -0,0 +1,142 @@
+/*
+ * 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.unomi.shell.dev.commands.scoring;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.segments.Scoring;
+import org.apache.unomi.api.services.SegmentService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class ScoringCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+            "itemId", "scope", "name", "description", "elements", "metadata"
+    );
+
+    @Reference
+    private SegmentService segmentService;
+
+    @Override
+    public String getObjectType() {
+        return "scoring";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[] {
+            "Activated",
+            "Hidden",
+            "Read-only",
+            "Identifier",
+            "Scope",
+            "Name",
+            "Tags",
+            "System tags"
+        };
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        return segmentService.getScoringMetadatas(query);
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        Metadata metadata = (Metadata) item;
+        ArrayList<Comparable> rowData = new ArrayList<>();
+        rowData.add(metadata.isEnabled() ? "x" : "");
+        rowData.add(metadata.isHidden() ? "x" : "");
+        rowData.add(metadata.isReadOnly() ? "x" : "");
+        rowData.add(metadata.getId());
+        rowData.add(metadata.getScope());
+        rowData.add(metadata.getName());
+        rowData.add(StringUtils.join(metadata.getTags(), ","));
+        rowData.add(StringUtils.join(metadata.getSystemTags(), ","));
+        return rowData.toArray(new Comparable[0]);
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        Scoring scoring = OBJECT_MAPPER.convertValue(properties, Scoring.class);
+        segmentService.setScoringDefinition(scoring);
+        return scoring.getItemId();
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        Scoring scoring = segmentService.getScoringDefinition(id);
+        if (scoring == null) {
+            return null;
+        }
+        return OBJECT_MAPPER.convertValue(scoring, Map.class);
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        Scoring scoring = segmentService.getScoringDefinition(id);
+        if (scoring == null) {
+            throw new IllegalArgumentException("Scoring with id '" + id + "' not found");
+        }
+        Scoring updatedScoring = OBJECT_MAPPER.convertValue(properties, Scoring.class);
+        updatedScoring.setItemId(id);
+        segmentService.setScoringDefinition(updatedScoring);
+    }
+
+    @Override
+    public void delete(String id) {
+        segmentService.removeScoringDefinition(id, false);
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return "Required properties:\n" +
+               "- itemId (string): Unique identifier for the scoring\n" +
+               "- scope (string): Scope of the scoring\n" +
+               "- name (string): Human-readable name\n" +
+               "\n" +
+               "Optional properties:\n" +
+               "- description (string): Description of the scoring\n" +
+               "- elements (array): List of scoring elements, each containing:\n" +
+               "  - condition (object): Condition that triggers the scoring element\n" +
+               "  - value (number): Score value to add when condition is met\n" +
+               "- enabled (boolean): Whether the scoring is enabled (default: true)\n" +
+               "- hidden (boolean): Whether the scoring is hidden (default: false)\n" +
+               "- readOnly (boolean): Whether the scoring is read-only (default: false)\n" +
+               "- tags (array): List of tags for the scoring\n" +
+               "- systemTags (array): List of system tags for the scoring";
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java
new file mode 100644
index 000000000..76446d359
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java
@@ -0,0 +1,195 @@
+/*
+ * 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.unomi.shell.dev.commands.segments;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.segments.Segment;
+import org.apache.unomi.api.services.SegmentService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.apache.unomi.api.conditions.Condition;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class SegmentCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+        "itemId", "name", "description", "scope", "condition", "metadata"
+    );
+    private static final List<String> CONDITION_TYPES = List.of(
+        "booleanCondition", "profilePropertyCondition", "sessionPropertyCondition", "eventPropertyCondition",
+        "pastEventCondition", "matchAllCondition", "notCondition", "orCondition", "andCondition",
+        "profileSegmentCondition", "scoringCondition"
+    );
+
+    @Reference
+    private SegmentService segmentService;
+
+    @Override
+    public String getObjectType() {
+        return "segment";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[] {
+            "Activated",
+            "Hidden",
+            "Read-only",
+            "Identifier",
+            "Scope",
+            "Name",
+            "Tags",
+            "System tags"
+        };
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        return segmentService.getSegmentMetadatas(query);
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        Metadata segmentMetadata = (Metadata) item;
+        ArrayList<Comparable> rowData = new ArrayList<>();
+        rowData.add(segmentMetadata.isEnabled() ? "x" : "");
+        rowData.add(segmentMetadata.isHidden() ? "x" : "");
+        rowData.add(segmentMetadata.isReadOnly() ? "x" : "");
+        rowData.add(segmentMetadata.getId());
+        rowData.add(segmentMetadata.getScope());
+        rowData.add(segmentMetadata.getName());
+        rowData.add(StringUtils.join(segmentMetadata.getTags(), ","));
+        rowData.add(StringUtils.join(segmentMetadata.getSystemTags(), ","));
+        return rowData.toArray(new Comparable[0]);
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        Segment segment = OBJECT_MAPPER.convertValue(properties, Segment.class);
+        segmentService.setSegmentDefinition(segment);
+        return segment.getItemId();
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        Segment segment = segmentService.getSegmentDefinition(id);
+        if (segment == null) {
+            return null;
+        }
+        return OBJECT_MAPPER.convertValue(segment, Map.class);
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        properties.put("itemId", id);
+        Segment segment = OBJECT_MAPPER.convertValue(properties, Segment.class);
+        segmentService.setSegmentDefinition(segment);
+    }
+
+    @Override
+    public void delete(String id) {
+        segmentService.removeSegmentDefinition(id, false);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return String.join("\n",
+            "Required properties:",
+            "- itemId: Segment ID (string)",
+            "- name: Segment name",
+            "- condition: Segment condition object",
+            "",
+            "Optional properties:",
+            "- description: Segment description",
+            "- scope: Segment scope",
+            "- metadata: Segment metadata",
+            "",
+            "Condition types:",
+            "- booleanCondition: Simple true/false condition",
+            "- profilePropertyCondition: Match profile property",
+            "- sessionPropertyCondition: Match session property",
+            "- eventPropertyCondition: Match event property",
+            "- pastEventCondition: Match past events",
+            "- matchAllCondition: Match all sub-conditions",
+            "- notCondition: Negate sub-condition",
+            "- orCondition: Match any sub-condition",
+            "- andCondition: Match all sub-conditions",
+            "- profileSegmentCondition: Match profile segment",
+            "- scoringCondition: Match scoring value"
+        );
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public List<String> completePropertyValue(String propertyName, String prefix) {
+        if ("condition.type".equals(propertyName)) {
+            return CONDITION_TYPES.stream()
+                    .filter(type -> type.startsWith(prefix))
+                    .collect(Collectors.toList());
+        } else if ("scope".equals(propertyName)) {
+            return List.of();
+        }
+        return List.of();
+    }
+
+    @Override
+    public List<String> completeId(String prefix) {
+        // Create a query to find segments that match the prefix
+        Query query = new Query();
+        query.setLimit(20); // Reasonable limit for auto-completion
+
+        try {
+            // If prefix is not empty, use it to filter results
+            if (!prefix.isEmpty()) {
+                Condition condition = new Condition(definitionsService.getConditionType("sessionPropertyCondition"));
+                condition.setParameter("comparisonOperator", "startsWith");
+                condition.setParameter("propertyName", "itemId");
+                condition.setParameter("propertyValue", prefix);
+                query.setCondition(condition);
+            } else {
+                // Otherwise, match all
+                Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition"));
+                query.setCondition(matchAllCondition);
+            }
+
+            // Execute the query and extract segment IDs
+            PartialList<Metadata> metadatas = segmentService.getSegmentMetadatas(query);
+            return metadatas.getList().stream()
+                .map(Metadata::getId)
+                .collect(Collectors.toList());
+        } catch (Exception e) {
+            return List.of(); // Return empty list on error
+        }
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java
new file mode 100644
index 000000000..6be61be42
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java
@@ -0,0 +1,128 @@
+/*
+ * 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.unomi.shell.dev.commands.sessions;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Session;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A command to perform CRUD operations on sessions
+ */
+@Component(service = CrudCommand.class, immediate = true)
+public class SessionCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+        "itemId", "profileId", "properties", "systemProperties", "timeStamp", "scope", "lastEventDate", "size", "duration", "originEventTypes", "originEventIds"
+    );
+
+    @Reference
+    private ProfileService profileService;
+
+    @Override
+    public String getObjectType() {
+        return "session";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[]{"ID", "Profile ID", "Scope", "Time Stamp", "Last Event", "Duration", "Size"};
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        return profileService.searchSessions(query);
+    }
+
+    @Override
+    protected Comparable[] buildRow(Object item) {
+        Session session = (Session) item;
+        return new Comparable[]{
+            session.getItemId(),
+            session.getProfileId(),
+            session.getScope(),
+            session.getTimeStamp() != null ? session.getTimeStamp().toString() : "",
+            session.getLastEventDate() != null ? session.getLastEventDate().toString() : "",
+            String.valueOf(session.getDuration()),
+            String.valueOf(session.getSize())
+        };
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        Session session = profileService.loadSession(id, null);
+        if (session == null) {
+            return null;
+        }
+        return OBJECT_MAPPER.convertValue(session, Map.class);
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        Session session = OBJECT_MAPPER.convertValue(properties, Session.class);
+        profileService.saveSession(session);
+        return session.getItemId();
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        Session updatedSession = OBJECT_MAPPER.convertValue(properties, Session.class);
+        updatedSession.setItemId(id);
+        profileService.saveSession(updatedSession);
+    }
+
+    @Override
+    public void delete(String id) {
+        profileService.deleteSession(id);
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return String.join("\n",
+            "Required properties:",
+            "- itemId: The unique identifier of the session",
+            "- profileId: The identifier of the associated profile",
+            "- timeStamp: The session creation timestamp (ISO-8601 format)",
+            "- scope: The scope of the session",
+            "",
+            "Optional properties:",
+            "- properties: A map of custom properties for the session",
+            "- systemProperties: A map of system properties for internal use",
+            "- lastEventDate: The date of the last event in the session (ISO-8601 format)",
+            "- size: The size of the session",
+            "- duration: The duration of the session in milliseconds",
+            "- originEventTypes: List of event types that caused the session creation",
+            "- originEventIds: List of event IDs that caused the session creation"
+        );
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java
new file mode 100644
index 000000000..f97bed28d
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java
@@ -0,0 +1,120 @@
+/*
+ * 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.unomi.shell.dev.commands.topics;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Topic;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.TopicService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class TopicCrudCommand extends BaseCrudCommand {
+
+    private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+    private static final List<String> PROPERTY_NAMES = List.of(
+        "itemId", "name", "description", "parentId", "metadata"
+    );
+
+    @Reference
+    private TopicService topicService;
+
+    @Override
+    public String getObjectType() {
+        return "topic";
+    }
+
+    @Override
+    protected String[] getHeadersWithoutTenant() {
+        return new String[] {
+            "Identifier",
+            "Name",
+            "Scope"
+        };
+    }
+
+    @Override
+    protected PartialList<?> getItems(Query query) {
+        return topicService.search(query);
+    }
+
+    @Override
+    protected String[] buildRow(Object item) {
+        Topic topic = (Topic) item;
+        return new String[] {
+            topic.getItemId(),
+            topic.getName(),
+            topic.getScope()
+        };
+    }
+
+    @Override
+    public String create(Map<String, Object> properties) {
+        Topic topic = OBJECT_MAPPER.convertValue(properties, Topic.class);
+        topicService.save(topic);
+        return topic.getItemId();
+    }
+
+    @Override
+    public Map<String, Object> read(String id) {
+        Topic topic = topicService.load(id);
+        if (topic == null) {
+            return null;
+        }
+        return OBJECT_MAPPER.convertValue(topic, Map.class);
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        properties.put("itemId", id);
+        Topic topic = OBJECT_MAPPER.convertValue(properties, Topic.class);
+        topicService.save(topic);
+    }
+
+    @Override
+    public void delete(String id) {
+        topicService.delete(id);
+    }
+
+    @Override
+    public String getPropertiesHelp() {
+        return String.join("\n",
+            "Required properties:",
+            "- itemId: Topic ID (string)",
+            "- name: Topic name",
+            "",
+            "Optional properties:",
+            "- description: Topic description",
+            "- parentId: Parent topic ID",
+            "- metadata: Topic metadata"
+        );
+    }
+
+    @Override
+    public List<String> completePropertyNames(String prefix) {
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/BaseCompleter.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/BaseCompleter.java
new file mode 100644
index 000000000..5581f519b
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/BaseCompleter.java
@@ -0,0 +1,86 @@
+/*
+ * 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.unomi.shell.dev.completers;
+
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import org.apache.unomi.api.Item;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.DefinitionsService;
+import org.apache.unomi.persistence.spi.PersistenceService;
+
+import java.io.PrintStream;
+import java.util.List;
+
+/**
+ * Base class for completers
+ */
+public abstract class BaseCompleter<T extends Item> implements Completer {
+    protected static final int DEFAULT_LIMIT = 50;
+
+    @Reference
+    protected PersistenceService persistenceService;
+
+    @Reference
+    protected DefinitionsService definitionsService;
+
+    protected abstract Class<T> getItemType();
+
+    protected abstract String getSortBy();
+
+    @Override
+    public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+        StringsCompleter delegate = new StringsCompleter();
+
+        try {
+            Query query = new Query();
+            query.setLimit(DEFAULT_LIMIT);
+            query.setSortby(getSortBy());
+
+            Condition condition = new Condition();
+            condition.setConditionType(definitionsService.getConditionType("matchAllCondition"));
+            query.setCondition(condition);
+
+            PartialList<T> items = persistenceService.query(query.getCondition(), query.getSortby(), getItemType(), query.getOffset(), query.getLimit());
+
+            for (T item : items.getList()) {
+                delegate.getStrings().add(item.getItemId());
+            }
+
+            return delegate.complete(session, commandLine, candidates);
+        } catch (Exception e) {
+            // Log error but don't fail completion
+            // Note: Printing during completion can interfere with completion, but using console for consistency
+            PrintStream console = session.getConsole();
+            console.println("Error: Error completing items: " + e.getMessage());
+            return -1;
+        }
+    }
+
+    public void setPersistenceService(PersistenceService persistenceService) {
+        this.persistenceService = persistenceService;
+    }
+
+    public void setDefinitionsService(DefinitionsService definitionsService) {
+        this.definitionsService = definitionsService;
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java
new file mode 100644
index 000000000..71f6df412
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java
@@ -0,0 +1,156 @@
+/*
+ * 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.unomi.shell.dev.completers;
+
+import org.apache.karaf.shell.api.action.lifecycle.Init;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+@Service
+public class IdCompleter implements Completer {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(IdCompleter.class.getName());
+
+    @Reference
+    private BundleContext bundleContext;
+
+    @Init
+    public void init() {
+        LOGGER.debug("IdCompleter initialized");
+    }
+
+    @Override
+    public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+        // Get the operation and type from the command line
+        // args[0] = "crud" (command name), args[1] = operation, args[2] = type, args[3+] = remaining
+        String operation = null;
+        String type = null;
+        String[] args = commandLine.getArguments();
+        if (args.length > 1) {
+            operation = args[1];
+        }
+        if (args.length > 2) {
+            type = args[2];
+        }
+        if (type == null) {
+            return -1;
+        }
+
+        // Determine if ID completion is appropriate based on operation
+        // ID completion is only appropriate for: read, delete, and update (first argument)
+        if (operation != null) {
+            String operationLower = operation.toLowerCase();
+            if (!"read".equals(operationLower) && !"delete".equals(operationLower) && !"update".equals(operationLower)) {
+                // For create, list, help - ID completion is not appropriate
+                return -1;
+            }
+        }
+
+        // Determine which argument we're completing based on args.length
+        // args[0] = "crud" (command name)
+        // args[1] = operation
+        // args[2] = type
+        // args[3+] = remaining (multi-valued)
+        // For read/delete: remaining[0] = ID (complete when args.length == 3, i.e., we're at remaining[0])
+        // For update: remaining[0] = ID, remaining[1] = JSON
+        //   - Complete IDs when args.length == 3 (completing remaining[0], which is the ID)
+        //   - Don't complete IDs when args.length >= 4 (completing remaining[1], which is JSON)
+        
+        // For update operation, if args.length >= 4, we're past the ID argument
+        // and are completing the JSON part, so don't complete IDs
+        if (operation != null && "update".equals(operation.toLowerCase()) && args.length >= 4) {
+            // We're past the ID argument, so we're completing JSON - don't complete IDs
+            return -1;
+        }
+        
+        // For read/delete/update with args.length == 3, we're completing the ID (remaining[0])
+        // The completer is attached to the "remaining" argument, so it will be called
+        // when we're at that position
+
+        // Find the CrudCommand for this type
+        try {
+            ServiceReference<?>[] refs = bundleContext.getAllServiceReferences(CrudCommand.class.getName(), null);
+            if (refs != null) {
+                for (ServiceReference<?> ref : refs) {
+                    CrudCommand cmd = (CrudCommand) bundleContext.getService(ref);
+                    try {
+                        if (cmd.getObjectType().equals(type)) {
+                            // Get the prefix from what the user has typed so far
+                            // StringsCompleter will handle the final matching, but we need prefix for server-side filtering
+                            String prefix = extractPrefixFromBuffer(commandLine);
+                            
+                            List<String> completions = cmd.completeId(prefix);
+
+                            StringsCompleter delegate = new StringsCompleter();
+                            delegate.getStrings().addAll(completions);
+                            return delegate.complete(session, commandLine, candidates);
+                        }
+                    } finally {
+                        bundleContext.ungetService(ref);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            // Log error but continue
+            // Note: Printing during completion can interfere with completion, but using console for consistency
+            // Only log if it's a serious error - avoid logging during normal completion
+            LOGGER.debug("Error getting completions", e);
+        }
+
+        return -1;
+    }
+
+    /**
+     * Extract the prefix from the command line buffer for completion.
+     * This extracts the last word being typed, skipping options that start with '-'.
+     *
+     * @param commandLine the command line
+     * @return the prefix to use for filtering completions, or empty string if none
+     */
+    private String extractPrefixFromBuffer(CommandLine commandLine) {
+        String buffer = commandLine.getBuffer();
+        if (buffer == null || buffer.trim().isEmpty()) {
+            return "";
+        }
+        
+        // Get the last word from the buffer (the current value being typed)
+        String trimmed = buffer.trim();
+        int lastSpace = trimmed.lastIndexOf(' ');
+        if (lastSpace < 0 || lastSpace >= trimmed.length() - 1) {
+            return "";
+        }
+        
+        String prefix = trimmed.substring(lastSpace + 1);
+        // Skip if it looks like an option
+        if (prefix.startsWith("-")) {
+            return "";
+        }
+        
+        return prefix;
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/OperationCompleter.java
similarity index 54%
rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java
rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/OperationCompleter.java
index 7a228be9f..7ae6e3c35 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/OperationCompleter.java
@@ -14,25 +14,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.completers;
 
-import org.apache.karaf.shell.api.action.Action;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
 import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.services.RulesService;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
 
-@Command(scope = "unomi", name = "rule-reset-stats", description = "This command will reset the rule statistics")
-@Service
-public class RuleResetStats implements Action {
+import java.util.List;
 
-    @Reference
-    RulesService rulesService;
+@Service
+public class OperationCompleter implements Completer {
+    private static final List<String> OPERATIONS = List.of("create", "read", "update", "delete", "list", "help");
 
     @Override
-    public Object execute() throws Exception {
-        rulesService.resetAllRuleStatistics();
-        System.out.println("Rule statistics successfully reset.");
-        return null;
+    public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+        StringsCompleter delegate = new StringsCompleter();
+        delegate.getStrings().addAll(OPERATIONS);
+        return delegate.complete(session, commandLine, candidates);
     }
 }
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/TypeCompleter.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/TypeCompleter.java
new file mode 100644
index 000000000..4a962c9c9
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/TypeCompleter.java
@@ -0,0 +1,66 @@
+/*
+ * 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.unomi.shell.dev.completers;
+
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+import java.io.PrintStream;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Service
+public class TypeCompleter implements Completer {
+
+    @Reference
+    private BundleContext bundleContext;
+
+    @Override
+    public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+        Set<String> types = new HashSet<>();
+        try {
+            ServiceReference<?>[] refs = bundleContext.getAllServiceReferences(CrudCommand.class.getName(), null);
+            if (refs != null) {
+                for (ServiceReference<?> ref : refs) {
+                    CrudCommand cmd = (CrudCommand) bundleContext.getService(ref);
+                    try {
+                        types.add(cmd.getObjectType());
+                    } finally {
+                        bundleContext.ungetService(ref);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            // Log error but continue
+            // Note: Printing during completion can interfere with completion, but using console for consistency
+            PrintStream console = session.getConsole();
+            console.println("Error: Error getting object types: " + e.getMessage());
+        }
+
+        StringsCompleter delegate = new StringsCompleter();
+        delegate.getStrings().addAll(types);
+        return delegate.complete(session, commandLine, candidates);
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java
new file mode 100644
index 000000000..97fee6b69
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java
@@ -0,0 +1,399 @@
+/*
+ * 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.unomi.shell.dev.services;
+
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVPrinter;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.support.table.ShellTable;
+
+import java.io.PrintStream;
+import org.apache.unomi.api.Item;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.conditions.ConditionType;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.DefinitionsService;
+import org.apache.unomi.common.DataTable;
+import org.apache.unomi.shell.dev.commands.ListCommandSupport;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Base class for CRUD command implementations that provides common functionality
+ * for listing objects in a tabular format.
+ */
+public abstract class BaseCrudCommand extends ListCommandSupport implements CrudCommand {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(BaseCrudCommand.class.getName());
+
+    @Reference
+    protected volatile DefinitionsService definitionsService;
+
+    @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false)
+    protected int maxEntries = 100;
+
+    @Option(name = "--csv", description = "Output in CSV format", required = false)
+    protected boolean csv;
+
+    @Override
+    protected DataTable buildDataTable() {
+        PrintStream console = getConsole();
+        try {
+            Query query = buildQuery(maxEntries);
+            PartialList<?> items = getItems(query);
+            
+            printPaginationWarning(items, console);
+
+            DataTable dataTable = new DataTable();
+            for (Object item : items.getList()) {
+                Comparable[] rowWithTenant = buildRowWithTenant(item);
+                dataTable.addRow(rowWithTenant);
+            }
+
+            return dataTable;
+        } catch (Exception e) {
+            LOGGER.error("Error building data table", e);
+            console.println("Error: " + e.getMessage());
+            return new DataTable();
+        }
+    }
+
+    /**
+     * Get the sort criteria for the query.
+     * Default implementation sorts by last modification date.
+     * Override to provide different sorting.
+     *
+     * @return sort criteria (e.g., "metadata.lastModified:desc")
+     */
+    protected String getSortBy() {
+        return "metadata.lastModified:desc";
+    }
+
+    /**
+     * Get items using the provided query.
+     * Implementations must override this to use their specific service.
+     *
+     * @param query the query to execute
+     * @return partial list of items
+     */
+    protected abstract PartialList<?> getItems(Query query);
+
+    /**
+     * Build a row for the data table from an item.
+     * Implementations must override this to extract the relevant properties.
+     *
+     * @param item the item to convert to a row
+     * @return array of values for the row
+     */
+    protected abstract Comparable[] buildRow(Object item);
+
+    /**
+     * Get the column headers for the list output table.
+     * This implementation automatically prepends "Tenant" as the first column header,
+     * matching how tenantId is automatically prepended to rows in buildDataTable() and buildRows().
+     * Subclasses should implement getHeadersWithoutTenant() to provide their specific headers.
+     * 
+     * Subclasses can override this method to provide custom header handling (e.g., to skip the tenant column
+     * for commands like TenantCrudCommand where it would be redundant).
+     *
+     * @return array of column headers with "Tenant" as the first element (unless overridden)
+     */
+    @Override
+    public String[] getHeaders() {
+        String[] headersWithoutTenant = getHeadersWithoutTenant();
+        String[] headersWithTenant = new String[headersWithoutTenant.length + 1];
+        headersWithTenant[0] = "Tenant";
+        System.arraycopy(headersWithoutTenant, 0, headersWithTenant, 1, headersWithoutTenant.length);
+        return headersWithTenant;
+    }
+
+    /**
+     * Get the column headers for the list output table without the "Tenant" column.
+     * Subclasses must implement this method to provide their specific headers.
+     * The "Tenant" column will be automatically prepended by getHeaders().
+     *
+     * @return array of column headers (without "Tenant")
+     */
+    protected abstract String[] getHeadersWithoutTenant();
+
+    /**
+     * Build a query with matchAllCondition and sort criteria.
+     * This is a common helper method used by buildDataTable(), buildRows(), buildCsvOutput(), and completeId().
+     *
+     * @param limit maximum number of entries
+     * @return the configured query
+     * @throws Exception if definitions service is not available or matchAllCondition cannot be found
+     */
+    protected Query buildQuery(int limit) throws Exception {
+        Query query = new Query();
+        query.setLimit(limit);
+        
+        if (definitionsService == null) {
+            throw new Exception("Definitions service is not available");
+        }
+        
+        ConditionType matchAllConditionType = definitionsService.getConditionType("matchAllCondition");
+        if (matchAllConditionType == null) {
+            throw new Exception("No matchAllCondition available");
+        }
+        
+        Condition matchAllCondition = new Condition(matchAllConditionType);
+        query.setCondition(matchAllCondition);
+        query.setSortby(getSortBy());
+        
+        return query;
+    }
+
+    /**
+     * Build a row array with tenant ID prepended as the first element.
+     * This is a common helper method used by buildDataTable(), buildRows(), and buildCsvOutput().
+     *
+     * @param item the item to build a row from
+     * @return array with tenant ID as first element, followed by row data
+     */
+    protected Comparable[] buildRowWithTenant(Object item) {
+        Comparable[] rowData = buildRow(item);
+        String tenantId = getTenantIdFromItem(item);
+        
+        // Create a new array with tenantId as the first element
+        Comparable[] rowWithTenant = new Comparable[rowData.length + 1];
+        rowWithTenant[0] = tenantId;
+        System.arraycopy(rowData, 0, rowWithTenant, 1, rowData.length);
+        
+        return rowWithTenant;
+    }
+
+    /**
+     * Print pagination warning if not all items were retrieved.
+     * This is a common helper method used by buildDataTable() and buildRows().
+     *
+     * @param items the partial list of items
+     * @param console console for output
+     */
+    protected void printPaginationWarning(PartialList<?> items, PrintStream console) {
+        if (items.getList().size() != items.getTotalSize()) {
+            console.println("WARNING : Only the first " + items.getPageSize() + " items have been retrieved, there are " + items.getTotalSize() + " items registered in total. Use the maxEntries parameter to retrieve more items");
+        }
+    }
+
+    @Override
+    public void buildRows(ShellTable table, int maxEntries) {
+        PrintStream console = getConsole();
+        try {
+            Query query = buildQuery(maxEntries);
+            PartialList<?> items = getItems(query);
+            
+            printPaginationWarning(items, console);
+
+            for (Object item : items.getList()) {
+                Comparable[] rowWithTenant = buildRowWithTenant(item);
+                table.addRow().addContent(rowWithTenant);
+            }
+        } catch (Exception e) {
+            console.println("Error: " + e.getMessage());
+            LOGGER.error("Error building rows", e);
+        }
+    }
+
+    /**
+     * Generate CSV output directly using commons-csv API.
+     * This method uses the same logic as buildRows() but outputs as CSV.
+     *
+     * @param console console for output
+     * @param headers column headers
+     * @param limit maximum number of entries
+     * @throws Exception if generation fails
+     */
+    public void buildCsvOutput(PrintStream console, String[] headers, int limit) throws Exception {
+        Query query = buildQuery(limit);
+        PartialList<?> items = getItems(query);
+        
+        // Generate CSV directly using commons-csv
+        CSVFormat csvFormat = CSVFormat.DEFAULT;
+        CSVPrinter printer = csvFormat.print(console);
+        
+        // Print header
+        printer.printRecord((Object[]) headers);
+        
+        // Print data rows
+        for (Object item : items.getList()) {
+            Comparable[] rowWithTenant = buildRowWithTenant(item);
+            
+            // Convert to List<String> for CSV printer
+            List<String> row = new ArrayList<>();
+            for (Comparable<?> cell : rowWithTenant) {
+                row.add(cell != null ? cell.toString() : "");
+            }
+            printer.printRecord(row.toArray());
+        }
+        
+        printer.flush();
+    }
+
+    /**
+     * Extract the tenant ID from an item.
+     *
+     * @param item the item to extract tenant ID from
+     * @return the tenant ID or a default value if it can't be determined
+     */
+    protected String getTenantIdFromItem(Object item) {
+        // Tenant column reserved for when tenant support is merged (Item#getTenantId, Tenant type, etc.).
+        return "n/a";
+    }
+
+    /**
+     * Default implementation of ID completion for all CRUD commands.
+     * This method fetches a limited number of items and filters their IDs based on the given prefix.
+     *
+     * @param prefix the prefix to filter IDs by
+     * @return a list of matching item IDs
+     */
+    @Override
+    public List<String> completeId(String prefix) {
+        try {
+            // Create a query with increased limit to provide more completions
+            Query query = buildQuery(50); // Higher limit for completions
+
+            // Get items using the appropriate service method
+            PartialList<?> items = getItems(query);
+
+            // Extract IDs from the items
+            List<String> ids = new ArrayList<>();
+            for (Object item : items.getList()) {
+                String id = extractIdFromItem(item);
+                if (id != null && (prefix.isEmpty() || id.startsWith(prefix))) {
+                    ids.add(id);
+                }
+            }
+
+            return ids;
+        } catch (Exception e) {
+            LOGGER.error("Error completing IDs", e);
+            return List.of();
+        }
+    }
+
+    /**
+     * Get the console PrintStream, falling back to System.out if session is not available.
+     * This is needed because CrudCommand services retrieved via bundleContext.getService()
+     * may not have session injected (they're not shell command instances).
+     *
+     * @return PrintStream for console output
+     */
+    protected PrintStream getConsole() {
+        if (session != null) {
+            return session.getConsole();
+        }
+        return System.out;
+    }
+
+    /**
+     * Apply pagination to a list of items based on query parameters.
+     * This is a helper method for implementations that need to paginate in-memory lists.
+     *
+     * @param items the full list of items
+     * @param query the query with offset and limit parameters
+     * @param <T> the type of items in the list
+     * @return a PartialList with paginated results
+     */
+    protected <T> PartialList<T> paginateList(List<T> items, Query query) {
+        Integer offset = query.getOffset();
+        Integer limit = query.getLimit();
+        int start = offset == null ? 0 : offset;
+        int size = limit == null ? items.size() : limit;
+        int end = Math.min(start + size, items.size());
+        
+        List<T> pagedItems = items.subList(start, end);
+        return new PartialList<>(pagedItems, start, pagedItems.size(), items.size(), PartialList.Relation.EQUAL);
+    }
+
+    /**
+     * Filter property names by prefix. This is a helper method for completePropertyNames implementations.
+     *
+     * @param propertyNames the list of property names to filter
+     * @param prefix the prefix to filter by
+     * @return filtered list of property names
+     */
+    protected List<String> filterPropertyNames(List<String> propertyNames, String prefix) {
+        return propertyNames.stream()
+                .filter(name -> name.startsWith(prefix))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Extract the ID from an item. This method attempts to extract the ID using common patterns.
+     * Subclasses can override this method to provide specialized ID extraction for specific item types.
+     *
+     * @param item the item to extract the ID from
+     * @return the extracted ID, or null if it couldn't be extracted
+     */
+    protected String extractIdFromItem(Object item) {
+        // Handle Item subclasses
+        if (item instanceof Item) {
+            return ((Item) item).getItemId();
+        }
+
+        // Handle Metadata objects
+        if (item instanceof Metadata) {
+            return ((Metadata) item).getId();
+        }
+
+        // Try reflection as a fallback
+        try {
+            // Try common getter method names for ID
+            for (String methodName : new String[]{"getId", "getItemId", "getIdentifier", "getKey", "getName"}) {
+                try {
+                    Method method = item.getClass().getMethod(methodName);
+                    Object result = method.invoke(item);
+                    if (result != null) {
+                        return result.toString();
+                    }
+                } catch (NoSuchMethodException e) {
+                    // Method doesn't exist, try the next one
+                }
+            }
+
+            // Try direct field access as a last resort
+            for (String fieldName : new String[]{"id", "itemId", "identifier", "key", "name"}) {
+                try {
+                    Field field = item.getClass().getDeclaredField(fieldName);
+                    field.setAccessible(true);
+                    Object value = field.get(item);
+                    if (value != null) {
+                        return value.toString();
+                    }
+                } catch (NoSuchFieldException e) {
+                    // Field doesn't exist, try the next one
+                }
+            }
+        } catch (Exception e) {
+            // Ignore reflection errors
+        }
+
+        return null;
+    }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java
new file mode 100644
index 000000000..4c088e906
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java
@@ -0,0 +1,206 @@
+/*
+ * 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.unomi.shell.dev.services;
+
+import org.apache.karaf.shell.support.table.ShellTable;
+
+import java.io.PrintStream;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Interface for implementing CRUD (Create, Read, Update, Delete) operations in Karaf shell commands for Unomi objects.
+ * This interface is designed to be implemented by OSGi components that provide shell command functionality for different
+ * types of Unomi objects (e.g., rules, segments, profiles, events).
+ *
+ * Implementations must:
+ * 1. Be annotated with @Component(service = CrudCommand.class, immediate = true)
+ * 2. Reference the appropriate Unomi service using @Reference
+ * 3. Handle JSON serialization/deserialization of objects
+ * 4. Provide property name and value completion for shell command auto-completion
+ * 5. Use server-side pagination and sorting in list operations to handle large datasets
+ * 6. Extend ListCommandSupport to implement rich tabular output
+ *
+ * The list functionality is provided by extending ListCommandSupport and implementing:
+ * 1. getHeaders() - Define column headers for the table
+ * 2. buildDataTable() - Build table data using Query with proper sorting and pagination
+ * 3. getSortBy() - Define sort criteria (default: metadata.lastModified:desc)
+ * 4. buildRow() - Convert an item to a table row
+ *
+ * Example usage in shell:
+ * unomi:crud create rule --file rule.json
+ * unomi:crud create rule -d={"itemId":"rule_id","enabled":true}
+ * unomi:crud read rule -i rule_id
+ * unomi:crud update rule -i rule_id --file updated_rule.json
+ * unomi:crud update rule -i rule_id -d={"itemId":"rule_id","enabled":false}
+ * unomi:crud delete rule -i rule_id
+ * unomi:crud list rule [--csv]
+ * unomi:crud help rule
+ */
+public interface CrudCommand {
+    /**
+     * Get the type of object this command handles. This is used to register the command
+     * for a specific object type in the Unomi shell command system.
+     *
+     * @return the object type identifier (e.g., "rule", "segment", "profile", "event")
+     */
+    String getObjectType();
+
+    /**
+     * Create a new object in Unomi from a map of properties. The properties are typically
+     * deserialized from a JSON file provided by the user.
+     *
+     * Implementations should:
+     * 1. Convert the properties map to the appropriate Unomi object type
+     * 2. Validate required properties are present
+     * 3. Set any default values or metadata
+     * 4. Save the object using the appropriate Unomi service
+     *
+     * @param properties the object properties from JSON
+     * @return the ID of the created object
+     * @throws IllegalArgumentException if required properties are missing or invalid
+     */
+    String create(Map<String, Object> properties);
+
+    /**
+     * Read an object by ID and return its properties. The properties will be serialized
+     * to JSON for display to the user.
+     *
+     * Implementations should:
+     * 1. Load the object using the appropriate Unomi service
+     * 2. Return null if the object doesn't exist
+     * 3. Convert the object to a map of properties
+     *
+     * @param id the object ID
+     * @return map of object properties, or null if not found
+     */
+    Map<String, Object> read(String id);
+
+    /**
+     * Update an existing object with new properties. The properties are typically
+     * deserialized from a JSON file provided by the user.
+     *
+     * Implementations should:
+     * 1. Ensure the ID in properties matches the target ID
+     * 2. Convert the properties map to the appropriate Unomi object type
+     * 3. Validate required properties are present
+     * 4. Update the object using the appropriate Unomi service
+     *
+     * @param id the object ID to update
+     * @param properties the new object properties from JSON
+     * @throws IllegalArgumentException if required properties are missing or invalid
+     */
+    void update(String id, Map<String, Object> properties);
+
+    /**
+     * Delete an object by ID.
+     *
+     * Implementations should:
+     * 1. Delete the object using the appropriate Unomi service
+     * 2. Handle any cleanup or cascading deletes if necessary
+     *
+     * @param id the object ID to delete
+     */
+    void delete(String id);
+
+    /**
+     * Get help text describing the properties supported by this object type.
+     * This is displayed when the user runs the help command.
+     *
+     * The help text should include:
+     * 1. Required properties with descriptions
+     * 2. Optional properties with descriptions
+     * 3. Property types and formats
+     * 4. Examples or additional notes
+     *
+     * @return formatted help text for object properties
+     */
+    String getPropertiesHelp();
+
+    /**
+     * Get completions for object IDs based on a prefix. Used for shell command
+     * auto-completion when the user is entering an object ID.
+     *
+     * Implementations should use their service's query capabilities to efficiently
+     * search for matching IDs rather than loading all objects.
+     *
+     * @param prefix the current input prefix to filter completions
+     * @return list of matching object IDs
+     */
+    default List<String> completeId(String prefix) {
+        // Implementations should override this with an efficient query
+        return List.of();
+    }
+
+    /**
+     * Get completions for property names based on a prefix. Used for shell command
+     * auto-completion when the user is editing a JSON file.
+     *
+     * Implementations should:
+     * 1. Define a static list of valid property names
+     * 2. Filter the list by the given prefix
+     *
+     * @param prefix the current input prefix to filter completions
+     * @return list of matching property names
+     */
+    default List<String> completePropertyNames(String prefix) {
+        return List.of(); // Default implementation returns no completions
+    }
+
+    /**
+     * Get completions for property values based on the property name and prefix.
+     * Used for shell command auto-completion when the user is editing a JSON file.
+     *
+     * Implementations should:
+     * 1. Handle specific property names that have predefined values
+     * 2. Filter the possible values by the given prefix
+     * 3. Return empty list for properties without predefined values
+     *
+     * @param propertyName the property being completed
+     * @param prefix the current input prefix to filter completions
+     * @return list of possible values for the property
+     */
+    default List<String> completePropertyValue(String propertyName, String prefix) {
+        return List.of(); // Default implementation returns no completions
+    }
+
+    /**
+     * Get the column headers for the list output table.
+     *
+     * @return array of column headers
+     */
+    String[] getHeaders();
+
+    /**
+     * Build the rows for the list output table.
+     *
+     * @param table the table to add rows to
+     * @param maxEntries maximum number of entries to include
+     */
+    void buildRows(ShellTable table, int maxEntries);
+
+    /**
+     * Generate CSV output directly using commons-csv API.
+     * This method uses the same logic as buildRows() but outputs as CSV.
+     *
+     * @param console console for output
+     * @param headers column headers
+     * @param limit maximum number of entries
+     * @throws Exception if generation fails
+     */
+    void buildCsvOutput(PrintStream console, String[] headers, int limit) throws Exception;
+}