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/05/06 04:20:12 UTC

(unomi) branch UNOMI-879-unified-crud-shell created (now 94ec7a359)

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

sergehuber pushed a change to branch UNOMI-879-unified-crud-shell
in repository https://gitbox.apache.org/repos/asf/unomi.git


      at 94ec7a359 UNOMI-879 Unified CRUD shell developer commands

This branch includes the following new commits:

     new 94ec7a359 UNOMI-879 Unified CRUD shell developer commands

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(unomi) 01/01: UNOMI-879 Unified CRUD shell developer commands

Posted by sh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

sergehuber pushed a commit to branch UNOMI-879-unified-crud-shell
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit 94ec7a3595977d7e1117693049e41cf79d33e01e
Author: Serge Huber <sh...@apache.org>
AuthorDate: Wed May 6 06:17:46 2026 +0200

    UNOMI-879 Unified CRUD shell developer commands
    
    - Replace legacy per-type list/view commands with unified unomi:crud routing,
      shared bases (BaseCrudCommand, CrudCommand), completers, and type-specific
      CRUD implementations under org.apache.unomi.shell.dev.
    - Relocate deployment/tail/watch helpers into dev.commands package.
    - EventService/ProfileService: add deleteEvent/deleteSession; implementations
      delegate to PersistenceService.remove (aligned with unomi-3-dev).
    - BOM: dependency adjustments needed by shell-dev-commands build.
---
 .../apache/unomi/api/services/EventService.java    |   7 +
 .../apache/unomi/api/services/ProfileService.java  |   7 +
 bom/pom.xml                                        |   6 +
 .../services/impl/events/EventServiceImpl.java     |   5 +
 .../services/impl/profiles/ProfileServiceImpl.java |   5 +
 tools/shell-dev-commands/pom.xml                   |  13 +
 .../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  | 608 +++++++++++++++++++++
 .../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         |  87 ++-
 .../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     |  97 ++++
 .../commands/actions/ActionTypeCrudCommand.java    | 157 ++++++
 .../commands/campaigns/CampaignCrudCommand.java    | 151 +++++
 .../campaigns/CampaignEventCrudCommand.java        | 143 +++++
 .../conditions/ConditionTypeCrudCommand.java       | 153 ++++++
 .../dev/commands/consents/ConsentCrudCommand.java  | 244 +++++++++
 .../dev/commands/events/EventCrudCommand.java      | 167 ++++++
 .../shell/dev/commands/goals/GoalCrudCommand.java  | 141 +++++
 .../dev/commands/personas/PersonaCrudCommand.java  | 150 +++++
 .../commands/profiles/ProfileAliasCrudCommand.java | 155 ++++++
 .../dev/commands/profiles/ProfileCrudCommand.java  | 183 +++++++
 .../properties/PropertyTypeCrudCommand.java        | 174 ++++++
 .../shell/dev/commands/rules/RuleCrudCommand.java  | 209 +++++++
 .../commands/rules/RuleStatisticsCrudCommand.java  | 135 +++++
 .../dev/commands/scopes/ScopeCrudCommand.java      | 139 +++++
 .../dev/commands/scoring/ScoringCrudCommand.java   | 142 +++++
 .../dev/commands/segments/SegmentCrudCommand.java  | 195 +++++++
 .../dev/commands/sessions/SessionCrudCommand.java  | 129 +++++
 .../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  | 400 ++++++++++++++
 .../unomi/shell/dev/services/CrudCommand.java      | 206 +++++++
 62 files changed, 5217 insertions(+), 1272 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..bd4c53706 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,13 @@ public interface ProfileService {
      */
     void removeProfileSessions(String profileId);
 
+    /**
+     * Deletes the session identified by the given identifier from persistence.
+     *
+     * @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/bom/pom.xml b/bom/pom.xml
index 17b0f3752..c63868276 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -270,6 +270,12 @@
                 <artifactId>httpclient-osgi</artifactId>
                 <version>${httpclient-osgi.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.apache.httpcomponents</groupId>
+                <artifactId>httpclient-osgi</artifactId>
+                <version>${httpclient-osgi.version}</version>
+                <type>bundle</type>
+            </dependency>
             <dependency>
                 <groupId>org.apache.kafka</groupId>
                 <artifactId>kafka-clients</artifactId>
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..e62a63758 100644
--- a/tools/shell-dev-commands/pom.xml
+++ b/tools/shell-dev-commands/pom.xml
@@ -92,6 +92,7 @@
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient-osgi</artifactId>
+            <type>bundle</type>
             <scope>provided</scope>
         </dependency>
         <dependency>
@@ -99,6 +100,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 +120,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..2ca8433f1
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java
@@ -0,0 +1,608 @@
+/*
+ * 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.getObjectType().equals(type)) {
+                try {
+                    executeOperation(cmd, operationLower, console);
+                    return true; // Handler found and executed
+                } finally {
+                    bundleContext.ungetService(ref);
+                }
+            }
+        }
+        return false; // No handler found
+    }
+
+    @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 72%
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..7bfdf79af 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)) {
@@ -137,7 +206,7 @@ public abstract class DeploymentCommandSupport implements Action {
             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;
             }
 
@@ -149,7 +218,7 @@ public abstract class DeploymentCommandSupport implements Action {
             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;
             }
 
@@ -159,14 +228,14 @@ public abstract class DeploymentCommandSupport implements Action {
         }
 
         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;
         }
 
@@ -194,7 +263,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..bb6fa5016
--- /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().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().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().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..6ed184d2b
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java
@@ -0,0 +1,97 @@
+/*
+ * 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 = "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.removeActionType(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:
+                Patch patch = readDefinition(definitionURL, Patch.class);
+                // patchService.patch(patch);
+                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/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..caa609f47
--- /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) {
+            return;
+        }
+
+        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..8096e9dcb
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.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.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.ArrayList;
+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) {
+        // There's no direct method to get a single campaign event, so we need to query for it
+        Query query = new Query();
+        Condition condition = new Condition();
+        condition.setConditionType(definitionsService.getConditionType("matchAllCondition"));
+        condition.setParameter("operator", "and");
+        condition.setParameter("subConditions", new ArrayList<>());
+        query.setCondition(condition);
+
+        PartialList<CampaignEvent> events = goalsService.getEvents(query);
+        CampaignEvent event = events.getList().stream()
+            .filter(e -> e.getItemId().equals(id))
+            .findFirst()
+            .orElse(null);
+
+        if (event == null) {
+            return null;
+        }
+        return OBJECT_MAPPER.convertValue(event, Map.class);
+    }
+
+    @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..ed59c4288
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java
@@ -0,0 +1,244 @@
+/*
+ * 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.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 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 SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
+    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) {
+        // Since consents are stored within profiles, we need to get all profiles and extract their consents
+        PartialList<Profile> profiles = profileService.search(query, Profile.class);
+        List<Map<String, Object>> consents = new ArrayList<>();
+
+        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(DATE_FORMAT);
+                    consentMap.put("profileId", profile.getItemId());
+                    consents.add(consentMap);
+                }
+            }
+        }
+
+        return new PartialList<Map<String, Object>>(consents, profiles.getOffset(), profiles.getPageSize(), profiles.getTotalSize(), PartialList.Relation.EQUAL);
+    }
+
+    @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);
+        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);
+
+            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);
+
+            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) {
+            // Handle error
+        }
+    }
+
+    @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..38a7e77ef
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java
@@ -0,0 +1,141 @@
+/*
+ * 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());
+        return new PartialList<>(goals, goals.size(), 0, goals.size(), null);
+    }
+
+    @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) {
+            return;
+        }
+
+        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..45e0b8845
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java
@@ -0,0 +1,150 @@
+/*
+ * 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;
+        return new String[] {
+            persona.getItemId(),
+            (String) persona.getProperty("firstName"),
+            (String) persona.getProperty("lastName"),
+            (String) persona.getProperty("email"),
+            (String) persona.getProperty("description"),
+            persona.getSystemProperties().get("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..64b27af14
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java
@@ -0,0 +1,155 @@
+/*
+ * 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) {
+        // Since there's no direct method to get a single alias, we'll need to search for it
+        // Search with a reasonable limit to find the alias by ID
+        PartialList<ProfileAlias> aliases = profileService.findProfileAliases(null, 0, 100, null);
+        ProfileAlias alias = aliases.getList().stream()
+            .filter(a -> a.getItemId().equals(id))
+            .findFirst()
+            .orElse(null);
+
+        if (alias == null) {
+            return null;
+        }
+        return OBJECT_MAPPER.convertValue(alias, Map.class);
+    }
+
+    @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..3dc1c6f0c
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java
@@ -0,0 +1,183 @@
+/*
+ * 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) {
+        Profile profile = new Profile();
+        profile.setProperties(properties);
+        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..64d523926
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java
@@ -0,0 +1,209 @@
+/*
+ * 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.unomi.api.rules.Rule;
+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.apache.unomi.api.conditions.Condition;
+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 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;
+
+    @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 Comparable[] buildRow(Object item) {
+        Rule rule = (Rule) item;
+        String ruleId = rule.getItemId();
+        Map<String,RuleStatistics> allRuleStatistics = 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..8a0507bdd
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java
@@ -0,0 +1,135 @@
+/*
+ * 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) {
+        // Note: RulesService doesn't provide a direct way to create rule statistics
+        // They are automatically managed by the rules engine
+        return null;
+    }
+
+    @Override
+    public void update(String id, Map<String, Object> properties) {
+        // Note: RulesService doesn't provide a direct way to update rule statistics
+        // They are automatically managed by the rules engine
+    }
+
+    @Override
+    public void delete(String id) {
+        // Note: RulesService doesn't provide a direct way to delete individual rule statistics
+        // They are automatically managed by the rules engine
+        // You can use resetAllRuleStatistics() to reset all statistics to zero
+        rulesService.resetAllRuleStatistics();
+    }
+
+    @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",
+            "",
+            "Note: Use 'unomi:crud rulestats reset' to reset all rule statistics to zero."
+        );
+    }
+}
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..85e16aecc
--- /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("booleanCondition"));
+                condition.setParameter("operator", "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..a185163d1
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java
@@ -0,0 +1,129 @@
+/*
+ * 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;
+import java.util.stream.Collectors;
+
+/**
+ * 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..f9b0b204d
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java
@@ -0,0 +1,400 @@
+/*
+ * 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.close();
+    }
+
+    /**
+     * 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
+        }
+
+        // If all else fails, use toString and hope it's meaningful
+        return item.toString();
+    }
+}
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;
+}