Просмотр исходного кода

impl 004-logseq-cli-verb-subcommands.md

rcmerci 1 день назад
Родитель
Сommit
c0b29c3d19

+ 43 - 8
deps/cli/src/logseq/cli/common/mcp/tools.cljs

@@ -23,10 +23,14 @@
 
 (defn list-properties
   "Main fn for ListProperties tool"
-  [db {:keys [expand]}]
+  [db {:keys [expand include-built-in] :as options}]
   (ensure-db-graph db)
+  (let [include-built-in? (if (contains? options :include-built-in) include-built-in true)]
   (->> (d/datoms db :avet :block/tags :logseq.class/Property)
        (map #(d/entity db (:e %)))
+       (remove (fn [e]
+                 (and (not include-built-in?)
+                      (ldb/built-in? e))))
        #_((fn [x] (prn :prop-keys (distinct (mapcat keys x))) x))
        (map (fn [e]
               (if expand
@@ -41,14 +45,18 @@
                   (:logseq.property/description e)
                   (update :logseq.property/description db-property/property-value-content))
                 {:block/title (:block/title e)
-                 :block/uuid (str (:block/uuid e))})))))
+                 :block/uuid (str (:block/uuid e))}))))))
 
 (defn list-tags
   "Main fn for ListTags tool"
-  [db {:keys [expand]}]
+  [db {:keys [expand include-built-in] :as options}]
   (ensure-db-graph db)
+  (let [include-built-in? (if (contains? options :include-built-in) include-built-in true)]
   (->> (d/datoms db :avet :block/tags :logseq.class/Tag)
        (map #(d/entity db (:e %)))
+       (remove (fn [e]
+                 (and (not include-built-in?)
+                      (ldb/built-in? e))))
        (map (fn [e]
               (if expand
                 (cond-> (into {} e)
@@ -66,7 +74,7 @@
                   (:logseq.property/description e)
                   (update :logseq.property/description db-property/property-value-content))
                 {:block/title (:block/title e)
-                 :block/uuid (str (:block/uuid e))})))))
+                 :block/uuid (str (:block/uuid e))}))))))
 
 (defn- get-page-blocks
   [db page-id]
@@ -100,13 +108,40 @@
                        (dissoc :block/children :block/page))
                   (get-page-blocks db (:db/id page)))}))
 
+(defn- parse-time
+  [value]
+  (cond
+    (number? value) value
+    (string? value) (let [ms (js/Date.parse value)]
+                      (when-not (js/isNaN ms) ms))
+    :else nil))
+
 (defn list-pages
   "Main fn for ListPages tool"
-  [db {:keys [expand]}]
+  [db {:keys [expand include-hidden include-journal journal-only created-after updated-after] :as options}]
   (ensure-db-graph db)
+  (let [include-hidden? (boolean include-hidden)
+        include-journal? (if (contains? options :include-journal) include-journal true)
+        journal-only? (boolean journal-only)
+        created-after-ms (parse-time created-after)
+        updated-after-ms (parse-time updated-after)]
   (->> (d/datoms db :avet :block/name)
        (map #(d/entity db (:e %)))
-       (remove entity-util/hidden?)
+       (remove (fn [e]
+                 (and (not include-hidden?)
+                      (entity-util/hidden? e))))
+       (remove (fn [e]
+                 (let [is-journal? (ldb/journal? e)]
+                   (cond
+                     journal-only? (not is-journal?)
+                     (false? include-journal?) is-journal?
+                     :else false))))
+       (remove (fn [e]
+                 (and created-after-ms
+                      (<= (:block/created-at e 0) created-after-ms))))
+       (remove (fn [e]
+                 (and updated-after-ms
+                      (<= (:block/updated-at e 0) updated-after-ms))))
        (map (fn [e]
               (if expand
                 (-> e
@@ -115,7 +150,7 @@
                     (select-keys [:block/uuid :block/title :block/created-at :block/updated-at])
                     (update :block/uuid str))
                 {:block/title (:block/title e)
-                 :block/uuid (str (:block/uuid e))})))))
+                 :block/uuid (str (:block/uuid e))}))))))
 
 ;; upsert-nodes tool
 ;; =================
@@ -406,4 +441,4 @@
   [conn operations* {:keys [dry-run] :as opts}]
   (let [import-edn (build-upsert-nodes-edn @conn operations*)]
     (when-not dry-run (import-edn-data conn import-edn))
-    (summarize-upsert-operations operations* opts)))
+    (summarize-upsert-operations operations* opts)))

+ 25 - 29
docs/agent-guide/004-logseq-cli-verb-subcommands.md

@@ -46,7 +46,6 @@ Group names and order:
 | list | page | List pages |
 | list | tag | List tags |
 | list | property | List properties |
-| list | block | List blocks |
 | add | block | Add blocks |
 | add | page | Create page |
 | remove | block | Remove block |
@@ -65,10 +64,10 @@ Common list options:
 | Option | Applies to | Purpose | Notes |
 | --- | --- | --- | --- |
 | --expand | page, tag, property | Include expanded metadata | Maps to existing api-list-* expand behavior. |
-| --limit N | page, tag, property, block | Limit results | Implemented in CLI after fetch unless server supports it. |
-| --offset N | page, tag, property, block | Offset results | Implemented in CLI after fetch unless server supports it. |
-| --sort FIELD | page, tag, property, block | Sort results | Field whitelist per type. |
-| --order asc|desc | page, tag, property, block | Sort direction | Defaults to asc. |
+| --limit N | page, tag, property | Limit results | Implemented in CLI after fetch unless server supports it. |
+| --offset N | page, tag, property | Offset results | Implemented in CLI after fetch unless server supports it. |
+| --sort FIELD | page, tag, property | Sort results | Field whitelist per type. |
+| --order asc|desc | page, tag, property | Sort direction | Defaults to asc. |
 | --output FORMAT | all | Output format | Existing output handling. |
 
 List page options:
@@ -132,26 +131,25 @@ Show has no subcommands and returns the block tree for a page or block.
 ## Plan
 
 1. Review current CLI command parsing and action routing in src/main/logseq/cli/commands.cljs to map block group behavior to verb-first commands.
-2. Add failing unit tests in src/test/logseq/cli/commands_test.cljs for verb-first help output and parse behavior for list, add, remove, search, and tree.
-3. Add failing unit tests that assert list subtype option parsing and validation for list page, list tag, list property, and list block.
-4. Add failing unit tests for add page, add tag, add property, remove tag, and remove property parse and validation behavior.
-5. Add failing unit tests that assert search defaults to all types and respects --type and --include-content options.
-6. Add failing unit tests that assert tree accepts --page or --block and rejects missing targets.
-7. Run bb dev:test -v logseq.cli.commands-test/test-parse-args and confirm failures are about the new verbs and options.
-8. Update src/main/logseq/cli/commands.cljs to replace block subcommands with verb-first entries and to add list subcommand group.
-9. Update summary helpers in src/main/logseq/cli/commands.cljs to show group help for list, add, and remove instead of block, and to render help groups as Graph Inspect and Edit first, Graph Management last.
-10. Update src/main/logseq/cli/main.cljs usage string to reflect the verb-first command surface.
-11. Add list option specs in src/main/logseq/cli/commands.cljs and update validation to enforce required args and mutually exclusive flags.
-12. Implement list actions in src/main/logseq/cli/commands.cljs that call existing thread-apis for pages, tags, and properties.
-13. Implement add page using thread-api/apply-outliner-ops with :create-page in src/main/logseq/cli/commands.cljs.
-14. Update search logic in src/main/logseq/cli/commands.cljs to query all resource types and honor --type, --tag, --sort, and --include-content.
-15. Update show logic in src/main/logseq/cli/commands.cljs to support --id, --uuid, --page-name, and --level.
-16. Add failing integration tests in src/test/logseq/cli/integration_test.cljs for list page, list tag, list property, add page, remove page, search all, and show.
-21. Run bb dev:test -v logseq.cli.integration-test/test-cli-list-and-search and confirm failures before implementation.
-22. Implement behavior for list, add, remove, search, and tree until all tests pass.
-23. Update docs/cli/logseq-cli.md with new verb-first commands and examples.
-24. Run bb dev:test -r logseq.cli.* and confirm 0 failures and 0 errors.
-25. Run bb dev:lint-and-test and confirm a zero exit code.
+2. Add failing unit tests in src/test/logseq/cli/commands_test.cljs for verb-first help output and parse behavior for list, add, remove, search, and show.
+3. Add failing unit tests that assert list subtype option parsing and validation for list page, list tag, and list property.
+4. Add failing unit tests that assert search defaults to all types and respects --type and --include-content options.
+5. Add failing unit tests that assert show accepts --page-name, --uuid, or --id and rejects missing targets.
+6. Run bb dev:test -v logseq.cli.commands-test/test-parse-args and confirm failures are about the new verbs and options.
+7. Update src/main/logseq/cli/commands.cljs to replace block subcommands with verb-first entries and to add list subcommand group.
+8. Update summary helpers in src/main/logseq/cli/commands.cljs to show group help for list, add, and remove instead of block, and to render help groups as Graph Inspect and Edit first, Graph Management last.
+9. Update src/main/logseq/cli/main.cljs usage string to reflect the verb-first command surface.
+10. Add list option specs in src/main/logseq/cli/commands.cljs and update validation to enforce required args and mutually exclusive flags.
+11. Implement list actions in src/main/logseq/cli/commands.cljs that call existing thread-apis for pages, tags, and properties.
+12. Implement add page using thread-api/apply-outliner-ops with :create-page in src/main/logseq/cli/commands.cljs.
+13. Update search logic in src/main/logseq/cli/commands.cljs to query all resource types and honor --type, --tag, --sort, and --include-content.
+14. Update show logic in src/main/logseq/cli/commands.cljs to support --id, --uuid, --page-name, and --level.
+15. Add failing integration tests in src/test/logseq/cli/integration_test.cljs for list page, list tag, list property, add page, remove page, search all, and show.
+16. Run bb dev:test -v logseq.cli.integration-test/test-cli-list-and-search and confirm failures before implementation.
+17. Implement behavior for list, add, remove, search, and show until all tests pass.
+18. Update docs/cli/logseq-cli.md with new verb-first commands and examples.
+19. Run bb dev:test -r logseq.cli.* and confirm 0 failures and 0 errors.
+20. Run bb dev:lint-and-test and confirm a zero exit code.
 
 ## Edge cases
 
@@ -178,7 +176,7 @@ Expected output includes failing assertions about the new verb-first commands an
 Run the integration tests in red phase.
 
 ```bash
-bb dev:test -v logseq.cli.integration-test/test-cli-list-and-search
+bb dev:test -v logseq.cli.integration-test/test-cli-list-add-search-show-remove
 ```
 
 Expected output includes failing assertions about list and search output and ends with a non-zero exit code.
@@ -216,8 +214,6 @@ The integration tests will create a temporary graph, add pages, tags, and proper
 
 ## Question
 
-Add tag, remove tag, add property, remove property are Implement Later.
-
-Rename for page, tag, and property is Implement Later.
+None.
 
 ---

+ 26 - 16
docs/cli/logseq-cli.md

@@ -51,24 +51,34 @@ Server commands:
 - `server stop --repo <name>` - stop db-worker-node for a graph
 - `server restart --repo <name>` - restart db-worker-node for a graph
 
-Block commands:
-- `block add --content <text> [--page <name>] [--parent <uuid>]` - add blocks; defaults to today’s journal page if no page is given
-- `block add --blocks <edn> [--page <name>] [--parent <uuid>]` - insert blocks via EDN vector
-- `block add --blocks-file <path> [--page <name>] [--parent <uuid>]` - insert blocks from an EDN file
-- `block remove --block <uuid>` - remove a block and its children
-- `block remove --page <name>` - remove a page and its children
-- `block search --text <query> [--limit <n>]` - search block titles (Datalog includes?)
-- `block tree --page <name> [--format text|json|edn]` - show page tree
-- `block tree --block <uuid> [--format text|json|edn]` - show block tree
+Inspect and edit commands:
+- `list page [--expand] [--limit <n>] [--offset <n>] [--sort <field>] [--order asc|desc]` - list pages
+- `list tag [--expand] [--limit <n>] [--offset <n>] [--sort <field>] [--order asc|desc]` - list tags
+- `list property [--expand] [--limit <n>] [--offset <n>] [--sort <field>] [--order asc|desc]` - list properties
+- `add block --content <text> [--page <name>] [--parent <uuid>]` - add blocks; defaults to today’s journal page if no page is given
+- `add block --blocks <edn> [--page <name>] [--parent <uuid>]` - insert blocks via EDN vector
+- `add block --blocks-file <path> [--page <name>] [--parent <uuid>]` - insert blocks from an EDN file
+- `add page --page <name>` - create a page
+- `remove block --block <uuid>` - remove a block and its children
+- `remove page --page <name>` - remove a page and its children
+- `search --text <query> [--type page|block|tag|property|all] [--include-content] [--limit <n>]` - search across pages, blocks, tags, and properties
+- `show --page-name <name> [--format text|json|edn] [--level <n>]` - show page tree
+- `show --uuid <uuid> [--format text|json|edn] [--level <n>]` - show block tree
+- `show --id <id> [--format text|json|edn] [--level <n>]` - show block tree by db/id
 
 Help output:
 
 ```
 Subcommands:
-  block add [options]     Add blocks
-  block remove [options]  Remove block or page
-  block search [options]  Search blocks
-  block tree [options]    Show tree
+  list page [options]      List pages
+  list tag [options]       List tags
+  list property [options]  List properties
+  add block [options]      Add blocks
+  add page [options]       Create page
+  remove block [options]   Remove block
+  remove page [options]    Remove page
+  search [options]         Search graph
+  show [options]           Show tree
 ```
 
 Output formats:
@@ -78,8 +88,8 @@ Examples:
 
 ```bash
 node ./static/logseq-cli.js graph create --repo demo
-node ./static/logseq-cli.js block add --page TestPage --content "hello world"
-node ./static/logseq-cli.js block search --text "hello"
-node ./static/logseq-cli.js block tree --page TestPage --format json --output json
+node ./static/logseq-cli.js add block --page TestPage --content "hello world"
+node ./static/logseq-cli.js search --text "hello"
+node ./static/logseq-cli.js show --page-name TestPage --format json --output json
 node ./static/logseq-cli.js server list
 ```

+ 614 - 89
src/main/logseq/cli/commands.cljs

@@ -37,19 +37,78 @@
    :page {:desc "Page name"}
    :parent {:desc "Parent block UUID for add"}})
 
-(def ^:private content-remove-spec
-  {:block {:desc "Block UUID"}
-   :page {:desc "Page name"}})
+(def ^:private add-page-spec
+  {:page {:desc "Page name"}})
 
-(def ^:private content-search-spec
+(def ^:private remove-block-spec
+  {:block {:desc "Block UUID"}})
+
+(def ^:private remove-page-spec
+  {:page {:desc "Page name"}})
+
+(def ^:private list-common-spec
+  {:expand {:desc "Include expanded metadata"
+            :coerce :boolean}
+   :limit {:desc "Limit results"
+           :coerce :long}
+   :offset {:desc "Offset results"
+            :coerce :long}
+   :sort {:desc "Sort field"}
+   :order {:desc "Sort order (asc, desc)"}})
+
+(def ^:private list-page-spec
+  (merge list-common-spec
+         {:include-journal {:desc "Include journal pages"
+                            :coerce :boolean}
+          :journal-only {:desc "Only journal pages"
+                         :coerce :boolean}
+          :include-hidden {:desc "Include hidden pages"
+                           :coerce :boolean}
+          :updated-after {:desc "Filter by updated-at (ISO8601)"}
+          :created-after {:desc "Filter by created-at (ISO8601)"}
+          :fields {:desc "Select output fields (comma separated)"}}))
+
+(def ^:private list-tag-spec
+  (merge list-common-spec
+         {:include-built-in {:desc "Include built-in tags"
+                             :coerce :boolean}
+          :with-properties {:desc "Include tag properties"
+                            :coerce :boolean}
+          :with-extends {:desc "Include tag extends"
+                         :coerce :boolean}
+          :fields {:desc "Select output fields (comma separated)"}}))
+
+(def ^:private list-property-spec
+  (merge list-common-spec
+         {:include-built-in {:desc "Include built-in properties"
+                             :coerce :boolean}
+          :with-classes {:desc "Include property classes"
+                         :coerce :boolean}
+          :with-type {:desc "Include property type"
+                      :coerce :boolean}
+          :fields {:desc "Select output fields (comma separated)"}}))
+
+(def ^:private search-spec
   {:text {:desc "Search text"}
+   :type {:desc "Search types (page, block, tag, property, all)"}
+   :tag {:desc "Restrict to a specific tag"}
    :limit {:desc "Limit results"
-           :coerce :long}})
-
-(def ^:private content-tree-spec
-  {:block {:desc "Block UUID"}
-   :page {:desc "Page name"}
-   :format {:desc "Output format (tree)"}})
+           :coerce :long}
+   :case-sensitive {:desc "Case sensitive search"
+                    :coerce :boolean}
+   :include-content {:desc "Search block content"
+                     :coerce :boolean}
+   :sort {:desc "Sort field (updated-at, created-at)"}
+   :order {:desc "Sort order (asc, desc)"}})
+
+(def ^:private show-spec
+  {:id {:desc "Block db/id"
+        :coerce :long}
+   :uuid {:desc "Block UUID"}
+   :page-name {:desc "Page name"}
+   :level {:desc "Limit tree depth"
+           :coerce :long}
+   :format {:desc "Output format (text, json, edn)"}})
 
 (defn- format-commands
   [table]
@@ -82,14 +141,21 @@
 
 (defn- top-level-summary
   [table]
-  (string/join "\n"
-               ["Usage: logseq-cli <command> [options]"
-                ""
-                "Commands:"
-                (format-commands table)
-                ""
-                "Options:"
-                (cli/format-opts {:spec global-spec})]))
+  (let [groups [{:title "Graph Inspect and Edit"
+                 :commands #{"list" "add" "remove" "search" "show"}}
+                {:title "Graph Management"
+                 :commands #{"graph" "server"}}]
+        render-group (fn [{:keys [title commands]}]
+                       (let [entries (filter #(contains? commands (first (:cmds %))) table)]
+                         (string/join "\n" [title (format-commands entries)])))]
+    (string/join "\n"
+                 ["Usage: logseq-cli <command> [options]"
+                  ""
+                  "Commands:"
+                  (string/join "\n\n" (map render-group groups))
+                  ""
+                  "Options:"
+                  (cli/format-opts {:spec global-spec})])))
 
 (defn- command-summary
   [{:keys [cmds spec]}]
@@ -145,6 +211,13 @@
            :message "block or page is required"}
    :summary summary})
 
+(defn- missing-page-name-result
+  [summary]
+  {:ok? false
+   :error {:code :missing-page-name
+           :message "page name is required"}
+   :summary summary})
+
 (defn- missing-search-result
   [summary]
   {:ok? false
@@ -172,6 +245,67 @@
            :message message}
    :summary summary})
 
+(def ^:private list-sort-fields
+  {:list-page #{"title" "created-at" "updated-at"}
+   :list-tag #{"name" "title"}
+   :list-property #{"name" "title"}})
+
+(def ^:private show-formats
+  #{"text" "json" "edn"})
+
+(def ^:private search-types
+  #{"page" "block" "tag" "property" "all"})
+
+(defn- invalid-list-options?
+  [command opts]
+  (let [{:keys [order include-journal journal-only]} opts
+        sort-field (:sort opts)
+        allowed (get list-sort-fields command)]
+    (cond
+      (and include-journal journal-only)
+      "include-journal and journal-only are mutually exclusive"
+
+      (and (seq sort-field) (not (contains? allowed sort-field)))
+      (str "invalid sort field: " sort-field)
+
+      (and (seq order) (not (#{"asc" "desc"} order)))
+      (str "invalid order: " order)
+
+      :else
+      nil)))
+
+(defn- invalid-show-options?
+  [opts]
+  (let [format (:format opts)
+        level (:level opts)]
+    (cond
+      (and (seq format) (not (contains? show-formats (string/lower-case format))))
+      (str "invalid format: " format)
+
+      (and (some? level) (< level 1))
+      "level must be >= 1"
+
+      :else
+      nil)))
+
+(defn- invalid-search-options?
+  [opts]
+  (let [type (:type opts)
+        order (:order opts)
+        sort-field (:sort opts)]
+    (cond
+      (and (seq type) (not (contains? search-types type)))
+      (str "invalid type: " type)
+
+      (and (seq sort-field) (not (#{"updated-at" "created-at"} sort-field)))
+      (str "invalid sort field: " sort-field)
+
+      (and (seq order) (not (#{"asc" "desc"} order)))
+      (str "invalid order: " order)
+
+      :else
+      nil)))
+
 (defn- command-entry
   [cmds command desc spec]
   (let [spec* (merge-spec spec)]
@@ -198,10 +332,15 @@
    (command-entry ["server" "start"] :server-start "Start db-worker-node for a graph" server-spec)
    (command-entry ["server" "stop"] :server-stop "Stop db-worker-node for a graph" server-spec)
    (command-entry ["server" "restart"] :server-restart "Restart db-worker-node for a graph" server-spec)
-   (command-entry ["block" "add"] :add "Add blocks" content-add-spec)
-   (command-entry ["block" "remove"] :remove "Remove block or page" content-remove-spec)
-   (command-entry ["block" "search"] :search "Search blocks" content-search-spec)
-   (command-entry ["block" "tree"] :tree "Show tree" content-tree-spec)])
+   (command-entry ["list" "page"] :list-page "List pages" list-page-spec)
+   (command-entry ["list" "tag"] :list-tag "List tags" list-tag-spec)
+   (command-entry ["list" "property"] :list-property "List properties" list-property-spec)
+   (command-entry ["add" "block"] :add-block "Add blocks" content-add-spec)
+   (command-entry ["add" "page"] :add-page "Create page" add-page-spec)
+   (command-entry ["remove" "block"] :remove-block "Remove block" remove-block-spec)
+   (command-entry ["remove" "page"] :remove-page "Remove page" remove-page-spec)
+   (command-entry ["search"] :search "Search graph" search-spec)
+   (command-entry ["show"] :show "Show tree" show-spec)])
 
 (def ^:private global-aliases
   (->> global-spec
@@ -258,7 +397,8 @@
         has-content? (or (seq (:content opts))
                          (seq (:blocks opts))
                          (seq (:blocks-file opts))
-                         has-args?)]
+                         has-args?)
+        show-targets (filter some? [(:id opts) (:uuid opts) (:page-name opts)])]
     (cond
       (:help opts)
       (help-result cmd-summary)
@@ -267,18 +407,37 @@
            (not (seq graph)))
       (missing-graph-result summary)
 
-      (and (= command :add) (not has-content?))
+      (and (= command :add-block) (not has-content?))
       (missing-content-result summary)
 
-      (and (= command :remove) (not (or (seq (:block opts)) (seq (:page opts)))))
+      (and (= command :add-page) (not (seq (:page opts))))
+      (missing-page-name-result summary)
+
+      (and (= command :remove-block) (not (seq (:block opts))))
+      (missing-target-result summary)
+
+      (and (= command :remove-page) (not (seq (:page opts))))
       (missing-target-result summary)
 
-      (and (= command :tree) (not (or (seq (:block opts)) (seq (:page opts)))))
+      (and (= command :show) (empty? show-targets))
       (missing-target-result summary)
 
+      (and (= command :show) (> (count show-targets) 1))
+      (invalid-options-result summary "only one of --id, --uuid, or --page-name is allowed")
+
       (and (= command :search) (not (or (seq (:text opts)) has-args?)))
       (missing-search-result summary)
 
+      (and (#{:list-page :list-tag :list-property} command)
+           (invalid-list-options? command opts))
+      (invalid-options-result summary (invalid-list-options? command opts))
+
+      (and (= command :show) (invalid-show-options? opts))
+      (invalid-options-result summary (invalid-show-options? opts))
+
+      (and (= command :search) (invalid-search-options? opts))
+      (invalid-options-result summary (invalid-search-options? opts))
+
       (and (#{:server-status :server-start :server-stop :server-restart} command)
            (not (seq (:repo opts))))
       (missing-repo-result summary)
@@ -301,7 +460,7 @@
          :error {:code :missing-command
                  :message "missing command"}
          :summary summary})
-      (if (and (= 1 (count args)) (#{"graph" "block" "server"} (first args)))
+      (if (and (= 1 (count args)) (#{"graph" "server" "list" "add" "remove"} (first args)))
         (help-result (group-summary (first args) table))
         (try
           (let [result (cli/dispatch table args {:spec global-spec})]
@@ -457,38 +616,56 @@
       (mapv first rows))))
 
 (defn- build-tree
-  [blocks root-id]
+  [blocks root-id max-depth]
   (let [parent->children (group-by #(get-in % [:block/parent :db/id]) blocks)
         sort-children (fn [children]
                         (vec (sort-by :block/order children)))
-        build (fn build [parent-id]
+        build (fn build [parent-id depth]
                 (mapv (fn [b]
-                        (let [children (build (:db/id b))]
+                        (let [children (build (:db/id b) (inc depth))]
                           (cond-> b
                             (seq children) (assoc :block/children children))))
-                      (sort-children (get parent->children parent-id))))]
-    (build root-id)))
+                      (if (and max-depth (>= depth max-depth))
+                        []
+                        (sort-children (get parent->children parent-id)))))]
+    (build root-id 1)))
 
 (defn- fetch-tree
-  [config {:keys [repo block page]}]
-  (if (seq block)
-    (if-not (common-util/uuid-string? block)
-      (p/rejected (ex-info "block must be a uuid" {:code :invalid-block}))
+  [config {:keys [repo id uuid page-name level]}]
+  (let [max-depth (or level 10)]
+    (cond
+      (some? id)
       (p/let [entity (transport/invoke config "thread-api/pull" false
-                                       [repo [:db/id :block/uuid :block/title {:block/page [:db/id :block/title]}]
-                                        [:block/uuid (uuid block)]])]
+                                       [repo [:db/id :block/uuid :block/title {:block/page [:db/id :block/title]}] id])]
         (if-let [page-id (get-in entity [:block/page :db/id])]
           (p/let [blocks (fetch-blocks-for-page config repo page-id)
-                  children (build-tree blocks (:db/id entity))]
+                  children (build-tree blocks (:db/id entity) max-depth)]
             {:root (assoc entity :block/children children)})
-          (throw (ex-info "block not found" {:code :block-not-found})))))
-    (p/let [page-entity (transport/invoke config "thread-api/pull" false
-                                          [repo [:db/id :block/uuid :block/title] [:block/name page]])]
-      (if-let [page-id (:db/id page-entity)]
-        (p/let [blocks (fetch-blocks-for-page config repo page-id)
-                children (build-tree blocks page-id)]
-          {:root (assoc page-entity :block/children children)})
-        (throw (ex-info "page not found" {:code :page-not-found}))))))
+          (throw (ex-info "block not found" {:code :block-not-found}))))
+
+      (seq uuid)
+      (if-not (common-util/uuid-string? uuid)
+        (p/rejected (ex-info "block must be a uuid" {:code :invalid-block}))
+        (p/let [entity (transport/invoke config "thread-api/pull" false
+                                         [repo [:db/id :block/uuid :block/title {:block/page [:db/id :block/title]}]
+                                          [:block/uuid (uuid uuid)]])]
+          (if-let [page-id (get-in entity [:block/page :db/id])]
+            (p/let [blocks (fetch-blocks-for-page config repo page-id)
+                    children (build-tree blocks (:db/id entity) max-depth)]
+              {:root (assoc entity :block/children children)})
+            (throw (ex-info "block not found" {:code :block-not-found})))))
+
+      (seq page-name)
+      (p/let [page-entity (transport/invoke config "thread-api/pull" false
+                                            [repo [:db/id :block/uuid :block/title] [:block/name page-name]])]
+        (if-let [page-id (:db/id page-entity)]
+          (p/let [blocks (fetch-blocks-for-page config repo page-id)
+                  children (build-tree blocks page-id max-depth)]
+            {:root (assoc page-entity :block/children children)})
+          (throw (ex-info "page not found" {:code :page-not-found}))))
+
+      :else
+      (p/rejected (ex-info "block or page required" {:code :missing-target})))))
 
 (defn- tree->text
   [{:keys [root]}]
@@ -521,6 +698,81 @@
    :error {:code :missing-repo
            :message message}})
 
+(def ^:private list-page-field-map
+  {"title" :block/title
+   "uuid" :block/uuid
+   "created-at" :block/created-at
+   "updated-at" :block/updated-at})
+
+(def ^:private list-tag-field-map
+  {"name" :block/title
+   "title" :block/title
+   "uuid" :block/uuid
+   "properties" :logseq.property.class/properties
+   "extends" :logseq.property.class/extends
+   "description" :logseq.property/description})
+
+(def ^:private list-property-field-map
+  {"name" :block/title
+   "title" :block/title
+   "uuid" :block/uuid
+   "classes" :logseq.property/classes
+   "type" :logseq.property/type
+   "description" :logseq.property/description})
+
+(defn- parse-field-list
+  [fields]
+  (when (seq fields)
+    (->> (string/split fields #",")
+         (map string/trim)
+         (remove string/blank?)
+         vec)))
+
+(defn- apply-fields
+  [items fields field-map]
+  (if (seq fields)
+    (let [keys (->> fields
+                    (map #(get field-map %))
+                    (remove nil?)
+                    vec)]
+      (if (seq keys)
+        (mapv #(select-keys % keys) items)
+        items))
+    items))
+
+(defn- apply-sort
+  [items sort-field order field-map]
+  (if (seq sort-field)
+    (let [sort-key (get field-map sort-field)
+          sorted (if sort-key
+                   (sort-by #(get % sort-key) items)
+                   items)
+          sorted (if (= "desc" order) (reverse sorted) sorted)]
+      (vec sorted))
+    (vec items)))
+
+(defn- apply-offset-limit
+  [items offset limit]
+  (cond-> items
+    (some? offset) (->> (drop offset) vec)
+    (some? limit) (->> (take limit) vec)))
+
+(defn- prepare-tag-item
+  [item {:keys [expand with-properties with-extends]}]
+  (if expand
+    (cond-> item
+      (not with-properties) (dissoc :logseq.property.class/properties)
+      (not with-extends) (dissoc :logseq.property.class/extends))
+    item))
+
+(defn- prepare-property-item
+  [item {:keys [expand with-classes with-type]}]
+  (if expand
+    (cond-> item
+      (not with-classes) (dissoc :logseq.property/classes)
+      (not with-type) (dissoc :logseq.property/type))
+    item))
+
 (defn- build-graph-action
   [command graph repo]
   (case command
@@ -615,7 +867,7 @@
      :error {:code :unknown-command
              :message (str "unknown server command: " command)}}))
 
-(defn- build-add-action
+(defn- build-add-block-action
   [options args repo]
   (if-not (seq repo)
     (missing-repo-error "repo is required for add")
@@ -626,28 +878,64 @@
           (if-not (:ok? vector-result)
             vector-result
             {:ok? true
-             :action {:type :add
+             :action {:type :add-block
                       :repo repo
                       :graph (repo->graph repo)
                       :page (:page options)
                       :parent (:parent options)
                       :blocks (:value vector-result)}}))))))
 
-(defn- build-remove-action
+(defn- build-add-page-action
+  [options repo]
+  (if-not (seq repo)
+    (missing-repo-error "repo is required for add")
+    (let [page (some-> (:page options) string/trim)]
+      (if (seq page)
+        {:ok? true
+         :action {:type :add-page
+                  :repo repo
+                  :graph (repo->graph repo)
+                  :page page}}
+        {:ok? false
+         :error {:code :missing-page-name
+                 :message "page name is required"}}))))
+
+(defn- build-remove-block-action
+  [options repo]
+  (if-not (seq repo)
+    (missing-repo-error "repo is required for remove")
+    (let [block (some-> (:block options) string/trim)]
+      (if (seq block)
+        {:ok? true
+         :action {:type :remove-block
+                  :repo repo
+                  :block block}}
+        {:ok? false
+         :error {:code :missing-target
+                 :message "block is required"}}))))
+
+(defn- build-remove-page-action
   [options repo]
   (if-not (seq repo)
     (missing-repo-error "repo is required for remove")
-    (let [block (:block options)
-          page (:page options)]
-      (if (or (seq block) (seq page))
+    (let [page (some-> (:page options) string/trim)]
+      (if (seq page)
         {:ok? true
-         :action {:type :remove
+         :action {:type :remove-page
                   :repo repo
-                  :block block
                   :page page}}
         {:ok? false
          :error {:code :missing-target
-                 :message "block or page is required"}}))))
+                 :message "page is required"}}))))
+
+(defn- build-list-action
+  [command options repo]
+  (if-not (seq repo)
+    (missing-repo-error "repo is required for list")
+    {:ok? true
+     :action {:type command
+              :repo repo
+              :options options}}))
 
 (defn- build-search-action
   [options args repo]
@@ -659,28 +947,35 @@
          :action {:type :search
                   :repo repo
                   :text text
-                  :limit (:limit options)}}
+                  :search-type (:type options)
+                  :tag (:tag options)
+                  :limit (:limit options)
+                  :case-sensitive (:case-sensitive options)
+                  :include-content (:include-content options)
+                  :sort (:sort options)
+                  :order (:order options)}}
         {:ok? false
          :error {:code :missing-search-text
                  :message "search text is required"}}))))
 
-(defn- build-tree-action
+(defn- build-show-action
   [options repo]
   (if-not (seq repo)
-    (missing-repo-error "repo is required for tree")
-    (let [block (:block options)
-          page (:page options)
-          target (or block page)]
-      (if (seq target)
-        {:ok? true
-         :action {:type :tree
-                  :repo repo
-                  :block block
-                  :page page
-                  :format (some-> (:format options) string/lower-case)}}
+    (missing-repo-error "repo is required for show")
+    (let [format (some-> (:format options) string/lower-case)
+          targets (filter some? [(:id options) (:uuid options) (:page-name options)])]
+      (if (empty? targets)
         {:ok? false
          :error {:code :missing-target
-                 :message "block or page is required"}}))))
+                 :message "block or page is required"}}
+        {:ok? true
+         :action {:type :show
+                  :repo repo
+                  :id (:id options)
+                  :uuid (:uuid options)
+                  :page-name (:page-name options)
+                  :level (:level options)
+                  :format format}}))))
 
 (defn build-action
   [parsed config]
@@ -697,17 +992,26 @@
         (:server-list :server-status :server-start :server-stop :server-restart)
         (build-server-action command server-repo)
 
-        :add
-        (build-add-action options args repo)
+        (:list-page :list-tag :list-property)
+        (build-list-action command options repo)
+
+        :add-block
+        (build-add-block-action options args repo)
+
+        :add-page
+        (build-add-page-action options repo)
+
+        :remove-block
+        (build-remove-block-action options repo)
 
-        :remove
-        (build-remove-action options repo)
+        :remove-page
+        (build-remove-page-action options repo)
 
         :search
         (build-search-action options args repo)
 
-        :tree
-        (build-tree-action options repo)
+        :show
+        (build-show-action options repo)
 
         {:ok? false
          :error {:code :unknown-command
@@ -760,7 +1064,57 @@
                 :logseq.kv/graph-created-at (:kv/value created)
                 :logseq.kv/schema-version (:kv/value schema)}})))
 
-(defn- execute-add
+(defn- execute-list-page
+  [action config]
+  (-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
+              options (:options action)
+              items (transport/invoke cfg "thread-api/api-list-pages" false
+                                      [(:repo action) options])
+              order (or (:order options) "asc")
+              fields (parse-field-list (:fields options))
+              sorted (apply-sort items (:sort options) order list-page-field-map)
+              limited (apply-offset-limit sorted (:offset options) (:limit options))
+              final (if (:expand options)
+                      (apply-fields limited fields list-page-field-map)
+                      limited)]
+        {:status :ok
+         :data {:items final}})))
+
+(defn- execute-list-tag
+  [action config]
+  (-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
+              options (:options action)
+              items (transport/invoke cfg "thread-api/api-list-tags" false
+                                      [(:repo action) options])
+              order (or (:order options) "asc")
+              fields (parse-field-list (:fields options))
+              prepared (mapv #(prepare-tag-item % options) items)
+              sorted (apply-sort prepared (:sort options) order list-tag-field-map)
+              limited (apply-offset-limit sorted (:offset options) (:limit options))
+              final (if (:expand options)
+                      (apply-fields limited fields list-tag-field-map)
+                      limited)]
+        {:status :ok
+         :data {:items final}})))
+
+(defn- execute-list-property
+  [action config]
+  (-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
+              options (:options action)
+              items (transport/invoke cfg "thread-api/api-list-properties" false
+                                      [(:repo action) options])
+              order (or (:order options) "asc")
+              fields (parse-field-list (:fields options))
+              prepared (mapv #(prepare-property-item % options) items)
+              sorted (apply-sort prepared (:sort options) order list-property-field-map)
+              limited (apply-offset-limit sorted (:offset options) (:limit options))
+              final (if (:expand options)
+                      (apply-fields limited fields list-property-field-map)
+                      limited)]
+        {:status :ok
+         :data {:items final}})))
+
+(defn- execute-add-block
   [action config]
   (-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
               target-id (resolve-add-target cfg action)
@@ -773,6 +1127,14 @@
         {:status :ok
          :data {:result result}})))
 
+(defn- execute-add-page
+  [action config]
+  (-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
+              ops [[:create-page [(:page action) {}]]]
+              result (transport/invoke cfg "thread-api/apply-outliner-ops" false [(:repo action) ops {}])]
+        {:status :ok
+         :data {:result result}})))
+
 (defn- execute-remove
   [action config]
   (-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
@@ -780,21 +1142,179 @@
         {:status :ok
          :data {:result result}})))
 
+(defn- query-pages
+  [cfg repo text case-sensitive?]
+  (let [query (if case-sensitive?
+                '[:find ?e ?title ?uuid ?updated ?created
+                  :in $ ?q
+                  :where
+                  [?e :block/name ?name]
+                  [?e :block/title ?title]
+                  [?e :block/uuid ?uuid]
+                  [(get-else $ ?e :block/updated-at 0) ?updated]
+                  [(get-else $ ?e :block/created-at 0) ?created]
+                  [(string/includes? ?title ?q)]]
+                '[:find ?e ?title ?uuid ?updated ?created
+                  :in $ ?q
+                  :where
+                  [?e :block/name ?name]
+                  [?e :block/title ?title]
+                  [?e :block/uuid ?uuid]
+                  [(get-else $ ?e :block/updated-at 0) ?updated]
+                  [(get-else $ ?e :block/created-at 0) ?created]
+                  [(string/includes? (string/lower-case ?title) ?q)]])
+        q* (if case-sensitive? text (string/lower-case text))]
+    (transport/invoke cfg "thread-api/q" false [repo [query q*]])))
+
+(defn- query-blocks
+  [cfg repo text case-sensitive? tag include-content?]
+  (let [has-tag? (seq tag)
+        content-attr (if include-content? :block/content :block/title)
+        query (cond
+                (and case-sensitive? has-tag?)
+                `[:find ?e ?value ?uuid ?updated ?created
+                  :in $ ?q ?tag-name
+                  :where
+                  [?tag :block/name ?tag-name]
+                  [?e :block/tags ?tag]
+                  [?e ~content-attr ?value]
+                  [(missing? $ ?e :block/name)]
+                  [?e :block/uuid ?uuid]
+                  [(get-else $ ?e :block/updated-at 0) ?updated]
+                  [(get-else $ ?e :block/created-at 0) ?created]
+                  [(string/includes? ?value ?q)]]
+
+                case-sensitive?
+                `[:find ?e ?value ?uuid ?updated ?created
+                  :in $ ?q
+                  :where
+                  [?e ~content-attr ?value]
+                  [(missing? $ ?e :block/name)]
+                  [?e :block/uuid ?uuid]
+                  [(get-else $ ?e :block/updated-at 0) ?updated]
+                  [(get-else $ ?e :block/created-at 0) ?created]
+                  [(string/includes? ?value ?q)]]
+
+                has-tag?
+                `[:find ?e ?value ?uuid ?updated ?created
+                  :in $ ?q ?tag-name
+                  :where
+                  [?tag :block/name ?tag-name]
+                  [?e :block/tags ?tag]
+                  [?e ~content-attr ?value]
+                  [(missing? $ ?e :block/name)]
+                  [?e :block/uuid ?uuid]
+                  [(get-else $ ?e :block/updated-at 0) ?updated]
+                  [(get-else $ ?e :block/created-at 0) ?created]
+                  [(string/includes? (string/lower-case ?value) ?q)]]
+
+                :else
+                `[:find ?e ?value ?uuid ?updated ?created
+                  :in $ ?q
+                  :where
+                  [?e ~content-attr ?value]
+                  [(missing? $ ?e :block/name)]
+                  [?e :block/uuid ?uuid]
+                  [(get-else $ ?e :block/updated-at 0) ?updated]
+                  [(get-else $ ?e :block/created-at 0) ?created]
+                  [(string/includes? (string/lower-case ?value) ?q)]])
+        q* (if case-sensitive? text (string/lower-case text))
+        tag-name (some-> tag string/lower-case)]
+    (if has-tag?
+      (transport/invoke cfg "thread-api/q" false [repo [query q* tag-name]])
+      (transport/invoke cfg "thread-api/q" false [repo [query q*]]))))
+
+(defn- normalize-search-types
+  [type]
+  (let [type (or type "all")]
+    (case type
+      "page" [:page]
+      "block" [:block]
+      "tag" [:tag]
+      "property" [:property]
+      [:page :block :tag :property])))
+
+(defn- search-sort-key
+  [item sort-field]
+  (case sort-field
+    "updated-at" (:updated-at item)
+    "created-at" (:created-at item)
+    nil))
+
 (defn- execute-search
   [action config]
   (-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
-              query '[:find ?e ?title
-                      :in $ ?q
-                      :where
-                      [?e :block/title ?title]
-                      [(clojure.string/includes? ?title ?q)]]
-              results (transport/invoke cfg "thread-api/q" false [(:repo action) [query (:text action)]])
-              mapped (mapv (fn [[id title]] {:db/id id :block/title title}) results)
-              limited (if (some? (:limit action)) (vec (take (:limit action) mapped)) mapped)]
+              types (normalize-search-types (:search-type action))
+              case-sensitive? (boolean (:case-sensitive action))
+              text (:text action)
+              tag (:tag action)
+              page-results (when (some #{:page} types)
+                             (p/let [rows (query-pages cfg (:repo action) text case-sensitive?)]
+                               (mapv (fn [[id title uuid updated created]]
+                                       {:type "page"
+                                        :db/id id
+                                        :title title
+                                        :uuid (str uuid)
+                                        :updated-at updated
+                                        :created-at created})
+                                     rows)))
+              include-content? (boolean (:include-content action))
+              block-results (when (some #{:block} types)
+                              (p/let [rows (query-blocks cfg (:repo action) text case-sensitive? tag include-content?)]
+                                (mapv (fn [[id content uuid updated created]]
+                                        {:type "block"
+                                         :db/id id
+                                         :content content
+                                         :uuid (str uuid)
+                                         :updated-at updated
+                                         :created-at created})
+                                      rows)))
+              tag-results (when (some #{:tag} types)
+                            (p/let [items (transport/invoke cfg "thread-api/api-list-tags" false
+                                                            [(:repo action) {:expand true :include-built-in true}])
+                                    q* (if case-sensitive? text (string/lower-case text))]
+                              (->> items
+                                   (filter (fn [item]
+                                             (let [title (:block/title item)]
+                                               (if case-sensitive?
+                                                 (string/includes? title q*)
+                                                 (string/includes? (string/lower-case title) q*)))))
+                                   (mapv (fn [item]
+                                           {:type "tag"
+                                            :title (:block/title item)
+                                            :uuid (:block/uuid item)})))))
+              property-results (when (some #{:property} types)
+                                 (p/let [items (transport/invoke cfg "thread-api/api-list-properties" false
+                                                                 [(:repo action) {:expand true :include-built-in true}])
+                                         q* (if case-sensitive? text (string/lower-case text))]
+                                   (->> items
+                                        (filter (fn [item]
+                                                  (let [title (:block/title item)]
+                                                    (if case-sensitive?
+                                                      (string/includes? title q*)
+                                                      (string/includes? (string/lower-case title) q*)))))
+                                        (mapv (fn [item]
+                                                {:type "property"
+                                                 :title (:block/title item)
+                                                 :uuid (:block/uuid item)})))))
+              results (->> (concat (or page-results [])
+                                   (or block-results [])
+                                   (or tag-results [])
+                                   (or property-results []))
+                           (distinct)
+                           vec)
+              sorted (if-let [sort-field (:sort action)]
+                       (let [order (or (:order action) "desc")]
+                         (->> results
+                              (sort-by #(search-sort-key % sort-field))
+                              (cond-> (= order "desc") reverse)
+                              vec))
+                       results)
+              limited (if (some? (:limit action)) (vec (take (:limit action) sorted)) sorted)]
         {:status :ok
          :data {:results limited}})))
 
-(defn- execute-tree
+(defn- execute-show
   [action config]
   (-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
               tree-data (fetch-tree cfg action)
@@ -858,10 +1378,15 @@
             :invoke (execute-invoke action config)
             :graph-switch (execute-graph-switch action config)
             :graph-info (execute-graph-info action config)
-            :add (execute-add action config)
-            :remove (execute-remove action config)
+            :list-page (execute-list-page action config)
+            :list-tag (execute-list-tag action config)
+            :list-property (execute-list-property action config)
+            :add-block (execute-add-block action config)
+            :add-page (execute-add-page action config)
+            :remove-block (execute-remove action config)
+            :remove-page (execute-remove action config)
             :search (execute-search action config)
-            :tree (execute-tree action config)
+            :show (execute-show action config)
             :server-list (execute-server-list action config)
             :server-status (execute-server-status action config)
             :server-start (execute-server-start action config)

+ 1 - 1
src/main/logseq/cli/main.cljs

@@ -12,7 +12,7 @@
   (string/join "\n"
                ["logseq-cli <command> [options]"
                 ""
-                "Commands: graph list, graph create, graph switch, graph remove, graph validate, graph info, server list, server status, server start, server stop, server restart, block add, block remove, block search, block tree"
+                "Commands: list page, list tag, list property, add block, add page, remove block, remove page, search, show, graph list, graph create, graph switch, graph remove, graph validate, graph info, server list, server status, server start, server stop, server restart"
                 ""
                 "Options:"
                 summary]))

+ 169 - 151
src/test/logseq/cli/commands_test.cljs

@@ -6,12 +6,18 @@
             [promesa.core :as p]))
 
 (deftest test-help-output
-  (testing "top-level help lists subcommand groups"
+  (testing "top-level help lists command groups"
     (let [result (commands/parse-args ["--help"])
           summary (:summary result)]
       (is (true? (:help? result)))
+      (is (string/includes? summary "Graph Inspect and Edit"))
+      (is (string/includes? summary "Graph Management"))
+      (is (string/includes? summary "list"))
+      (is (string/includes? summary "add"))
+      (is (string/includes? summary "remove"))
+      (is (string/includes? summary "search"))
+      (is (string/includes? summary "show"))
       (is (string/includes? summary "graph"))
-      (is (string/includes? summary "block"))
       (is (string/includes? summary "server")))))
 
 (deftest test-parse-args
@@ -22,12 +28,27 @@
       (is (string/includes? summary "graph list"))
       (is (string/includes? summary "graph create"))))
 
-  (testing "block group shows subcommands"
-    (let [result (commands/parse-args ["block"])
+  (testing "list group shows subcommands"
+    (let [result (commands/parse-args ["list"])
           summary (:summary result)]
       (is (true? (:help? result)))
-      (is (string/includes? summary "block add"))
-      (is (string/includes? summary "block search"))))
+      (is (string/includes? summary "list page"))
+      (is (string/includes? summary "list tag"))
+      (is (string/includes? summary "list property"))))
+
+  (testing "add group shows subcommands"
+    (let [result (commands/parse-args ["add"])
+          summary (:summary result)]
+      (is (true? (:help? result)))
+      (is (string/includes? summary "add block"))
+      (is (string/includes? summary "add page"))))
+
+  (testing "remove group shows subcommands"
+    (let [result (commands/parse-args ["remove"])
+          summary (:summary result)]
+      (is (true? (:help? result)))
+      (is (string/includes? summary "remove block"))
+      (is (string/includes? summary "remove page"))))
 
   (testing "server group shows subcommands"
     (let [result (commands/parse-args ["server"])
@@ -51,23 +72,8 @@
       (is (seq subcommand-lines))
       (is (apply = desc-starts))))
 
-  (testing "block group aligns subcommand columns"
-    (let [result (commands/parse-args ["block"])
-          summary (:summary result)
-          subcommand-lines (let [lines (string/split-lines summary)
-                                 start (inc (.indexOf lines "Subcommands:"))]
-                             (->> lines
-                                  (drop start)
-                                  (take-while (complement string/blank?))))
-          desc-starts (->> subcommand-lines
-                           (keep (fn [line]
-                                   (when-let [[_ desc] (re-matches #"^\s+.*?\s{2,}(.+)$" line)]
-                                     (.indexOf line desc)))))]
-      (is (seq subcommand-lines))
-      (is (apply = desc-starts))))
-
-  (testing "server group aligns subcommand columns"
-    (let [result (commands/parse-args ["server"])
+  (testing "list group aligns subcommand columns"
+    (let [result (commands/parse-args ["list"])
           summary (:summary result)
           subcommand-lines (let [lines (string/split-lines summary)
                                  start (inc (.indexOf lines "Subcommands:"))]
@@ -83,21 +89,21 @@
 
   (testing "rejects legacy commands"
     (doseq [command ["graph-list" "graph-create" "graph-switch" "graph-remove"
-                     "graph-validate" "graph-info" "add" "remove" "search" "tree"
+                     "graph-validate" "graph-info" "block" "tree"
                      "ping" "status" "query" "export"]]
       (let [result (commands/parse-args [command])]
         (is (false? (:ok? result)))
         (is (= :unknown-command (get-in result [:error :code]))))))
 
-  (testing "rejects removed commands"
-    (let [result (commands/parse-args ["graph" "wat"])]
-      (is (false? (:ok? result)))
-      (is (= :unknown-command (get-in result [:error :code])))))
-
   (testing "rejects removed group commands"
-    (let [result (commands/parse-args ["content" "add"])]
-      (is (false? (:ok? result)))
-      (is (= :unknown-command (get-in result [:error :code])))))
+    (doseq [args [["block" "add"]
+                  ["block" "remove"]
+                  ["block" "search"]
+                  ["block" "tree"]
+                  ["content" "add"]]]
+      (let [result (commands/parse-args args)]
+        (is (false? (:ok? result)))
+        (is (= :unknown-command (get-in result [:error :code]))))))
 
   (testing "errors on missing command"
     (let [result (commands/parse-args [])]
@@ -114,153 +120,153 @@
       (is (true? (:ok? result)))
       (is (= "json" (get-in result [:options :output]))))))
 
-(deftest test-graph-subcommand-parse
-  (testing "graph list parses"
-    (let [result (commands/parse-args ["graph" "list"])]
+(deftest test-list-subcommand-parse
+  (testing "list page parses"
+    (let [result (commands/parse-args ["list" "page"
+                                       "--expand"
+                                       "--include-journal"
+                                       "--limit" "10"
+                                       "--offset" "5"
+                                       "--sort" "updated-at"
+                                       "--order" "desc"
+                                       "--fields" "title,updated-at"])]
       (is (true? (:ok? result)))
-      (is (= :graph-list (:command result)))))
-
-  (testing "graph create requires repo option"
-    (let [result (commands/parse-args ["graph" "create"])]
-      (is (false? (:ok? result)))
-      (is (= :missing-graph (get-in result [:error :code])))))
-
-  (testing "graph create parses with repo option"
-    (let [result (commands/parse-args ["graph" "create" "--repo" "demo"])]
+      (is (= :list-page (:command result)))
+      (is (true? (get-in result [:options :expand])))
+      (is (true? (get-in result [:options :include-journal])))
+      (is (= 10 (get-in result [:options :limit])))
+      (is (= 5 (get-in result [:options :offset])))
+      (is (= "updated-at" (get-in result [:options :sort])))
+      (is (= "desc" (get-in result [:options :order])))
+      (is (= "title,updated-at" (get-in result [:options :fields])))))
+
+  (testing "list tag parses"
+    (let [result (commands/parse-args ["list" "tag"
+                                       "--expand"
+                                       "--include-built-in"
+                                       "--with-properties"
+                                       "--with-extends"
+                                       "--fields" "name,properties"])]
       (is (true? (:ok? result)))
-      (is (= :graph-create (:command result)))
-      (is (= "demo" (get-in result [:options :repo])))))
-
-  (testing "graph switch requires repo option"
-    (let [result (commands/parse-args ["graph" "switch"])]
-      (is (false? (:ok? result)))
-      (is (= :missing-graph (get-in result [:error :code])))))
-
-  (testing "graph switch parses with repo option"
-    (let [result (commands/parse-args ["graph" "switch" "--repo" "demo"])]
+      (is (= :list-tag (:command result)))
+      (is (true? (get-in result [:options :expand])))
+      (is (true? (get-in result [:options :include-built-in])))
+      (is (true? (get-in result [:options :with-properties])))
+      (is (true? (get-in result [:options :with-extends])))
+      (is (= "name,properties" (get-in result [:options :fields])))))
+
+  (testing "list property parses"
+    (let [result (commands/parse-args ["list" "property"
+                                       "--expand"
+                                       "--include-built-in"
+                                       "--with-classes"
+                                       "--with-type"
+                                       "--fields" "name,type"])]
       (is (true? (:ok? result)))
-      (is (= :graph-switch (:command result)))
-      (is (= "demo" (get-in result [:options :repo])))))
-
-  (testing "graph remove requires repo option"
-    (let [result (commands/parse-args ["graph" "remove"])]
+      (is (= :list-property (:command result)))
+      (is (true? (get-in result [:options :expand])))
+      (is (true? (get-in result [:options :include-built-in])))
+      (is (true? (get-in result [:options :with-classes])))
+      (is (true? (get-in result [:options :with-type])))
+      (is (= "name,type" (get-in result [:options :fields]))))))
+
+(deftest test-list-subcommand-validation
+  (testing "list page rejects mutually exclusive journal flags"
+    (let [result (commands/parse-args ["list" "page"
+                                       "--include-journal"
+                                       "--journal-only"])]
       (is (false? (:ok? result)))
-      (is (= :missing-graph (get-in result [:error :code])))))
-
-  (testing "graph remove parses with repo option"
-    (let [result (commands/parse-args ["graph" "remove" "--repo" "demo"])]
-      (is (true? (:ok? result)))
-      (is (= :graph-remove (:command result)))
-      (is (= "demo" (get-in result [:options :repo])))))
+      (is (= :invalid-options (get-in result [:error :code])))))
 
-  (testing "graph validate requires repo option"
-    (let [result (commands/parse-args ["graph" "validate"])]
+  (testing "list page rejects invalid sort field"
+    (let [result (commands/parse-args ["list" "page" "--sort" "wat"])]
       (is (false? (:ok? result)))
-      (is (= :missing-graph (get-in result [:error :code])))))
-
-  (testing "graph validate parses with repo option"
-    (let [result (commands/parse-args ["graph" "validate" "--repo" "demo"])]
-      (is (true? (:ok? result)))
-      (is (= :graph-validate (:command result)))
-      (is (= "demo" (get-in result [:options :repo])))))
-
-  (testing "graph info parses without repo option"
-    (let [result (commands/parse-args ["graph" "info"])]
-      (is (true? (:ok? result)))
-      (is (= :graph-info (:command result)))))
-
-  (testing "graph info parses with repo option"
-    (let [result (commands/parse-args ["graph" "info" "--repo" "demo"])]
-      (is (true? (:ok? result)))
-      (is (= :graph-info (:command result)))
-      (is (= "demo" (get-in result [:options :repo])))))
+      (is (= :invalid-options (get-in result [:error :code])))))
 
-  (testing "graph subcommands reject unknown flags"
-    (doseq [subcommand ["list" "create" "switch" "remove" "validate" "info"]]
-      (let [result (commands/parse-args ["graph" subcommand "--wat"])]
-        (is (false? (:ok? result)))
-        (is (= :invalid-options (get-in result [:error :code]))))))
-
-
-  (testing "graph subcommands accept output option"
-    (let [result (commands/parse-args ["graph" "list" "--output" "edn"])]
-      (is (true? (:ok? result)))
-      (is (= "edn" (get-in result [:options :output])))))
-
-  (testing "server list parses"
-    (let [result (commands/parse-args ["server" "list"])]
-      (is (true? (:ok? result)))
-      (is (= :server-list (:command result)))))
+  (testing "list tag rejects invalid sort field"
+    (let [result (commands/parse-args ["list" "tag" "--sort" "wat"])]
+      (is (false? (:ok? result)))
+      (is (= :invalid-options (get-in result [:error :code])))))
 
-  (testing "server start requires repo"
-    (let [result (commands/parse-args ["server" "start"])]
+  (testing "list property rejects invalid sort field"
+    (let [result (commands/parse-args ["list" "property" "--sort" "wat"])]
       (is (false? (:ok? result)))
-      (is (= :missing-repo (get-in result [:error :code])))))
+      (is (= :invalid-options (get-in result [:error :code]))))))
 
-  (testing "server start parses with repo"
-    (let [result (commands/parse-args ["server" "start" "--repo" "demo"])]
-      (is (true? (:ok? result)))
-      (is (= :server-start (:command result)))
-      (is (= "demo" (get-in result [:options :repo])))))
+(deftest test-verb-subcommand-parse
+  (testing "add block requires content source"
+    (let [result (commands/parse-args ["add" "block"])]
+      (is (false? (:ok? result)))
+      (is (= :missing-content (get-in result [:error :code])))))
 
-  (testing "server stop parses with repo"
-    (let [result (commands/parse-args ["server" "stop" "--repo" "demo"])]
+  (testing "add block parses with content"
+    (let [result (commands/parse-args ["add" "block" "--content" "hello"])]
       (is (true? (:ok? result)))
-      (is (= :server-stop (:command result))))))
+      (is (= :add-block (:command result)))
+      (is (= "hello" (get-in result [:options :content])))))
 
-(deftest test-block-subcommand-parse
-  (testing "block add requires content source"
-    (let [result (commands/parse-args ["block" "add"])]
+  (testing "add page requires page name"
+    (let [result (commands/parse-args ["add" "page"])]
       (is (false? (:ok? result)))
-      (is (= :missing-content (get-in result [:error :code])))))
+      (is (= :missing-page-name (get-in result [:error :code])))))
 
-  (testing "block add parses with content"
-    (let [result (commands/parse-args ["block" "add" "--content" "hello"])]
+  (testing "add page parses with name"
+    (let [result (commands/parse-args ["add" "page" "--page" "Home"])]
       (is (true? (:ok? result)))
-      (is (= :add (:command result)))
-      (is (= "hello" (get-in result [:options :content])))))
+      (is (= :add-page (:command result)))
+      (is (= "Home" (get-in result [:options :page])))))
 
-  (testing "block remove requires target"
-    (let [result (commands/parse-args ["block" "remove"])]
+  (testing "remove block requires target"
+    (let [result (commands/parse-args ["remove" "block"])]
       (is (false? (:ok? result)))
       (is (= :missing-target (get-in result [:error :code])))))
 
-  (testing "block remove parses with block"
-    (let [result (commands/parse-args ["block" "remove" "--block" "demo"])]
+  (testing "remove block parses with block"
+    (let [result (commands/parse-args ["remove" "block" "--block" "demo"])]
       (is (true? (:ok? result)))
-      (is (= :remove (:command result)))
+      (is (= :remove-block (:command result)))
       (is (= "demo" (get-in result [:options :block])))))
 
-  (testing "block search requires text"
-    (let [result (commands/parse-args ["block" "search"])]
+  (testing "remove page parses with page"
+    (let [result (commands/parse-args ["remove" "page" "--page" "Home"])]
+      (is (true? (:ok? result)))
+      (is (= :remove-page (:command result)))
+      (is (= "Home" (get-in result [:options :page])))))
+
+  (testing "search requires text"
+    (let [result (commands/parse-args ["search"])]
       (is (false? (:ok? result)))
       (is (= :missing-search-text (get-in result [:error :code])))))
 
-  (testing "block search parses with text"
-    (let [result (commands/parse-args ["block" "search" "--text" "hello"])]
+  (testing "search parses with text"
+    (let [result (commands/parse-args ["search" "--text" "hello"])]
       (is (true? (:ok? result)))
       (is (= :search (:command result)))
       (is (= "hello" (get-in result [:options :text])))))
 
-  (testing "block tree requires target"
-    (let [result (commands/parse-args ["block" "tree"])]
+  (testing "show requires target"
+    (let [result (commands/parse-args ["show"])]
       (is (false? (:ok? result)))
       (is (= :missing-target (get-in result [:error :code])))))
 
-  (testing "block tree parses with page"
-    (let [result (commands/parse-args ["block" "tree" "--page" "Home"])]
+  (testing "show parses with page name"
+    (let [result (commands/parse-args ["show" "--page-name" "Home"])]
       (is (true? (:ok? result)))
-      (is (= :tree (:command result)))
-      (is (= "Home" (get-in result [:options :page])))))
-
-  (testing "block subcommands reject unknown flags"
-    (doseq [subcommand ["add" "remove" "search" "tree"]]
-      (let [result (commands/parse-args ["block" subcommand "--wat"])]
+      (is (= :show (:command result)))
+      (is (= "Home" (get-in result [:options :page-name])))))
+
+  (testing "verb subcommands reject unknown flags"
+    (doseq [args [["list" "page" "--wat"]
+                  ["add" "block" "--wat"]
+                  ["remove" "block" "--wat"]
+                  ["search" "--wat"]
+                  ["show" "--wat"]]]
+      (let [result (commands/parse-args args)]
         (is (false? (:ok? result)))
         (is (= :invalid-options (get-in result [:error :code]))))))
 
-  (testing "block subcommands accept output option"
-    (let [result (commands/parse-args ["block" "search" "--text" "hello" "--output" "json"])]
+  (testing "verb subcommands accept output option"
+    (let [result (commands/parse-args ["search" "--text" "hello" "--output" "json"])]
       (is (true? (:ok? result)))
       (is (= "json" (get-in result [:options :output]))))))
 
@@ -307,20 +313,32 @@
       (is (true? (:ok? result)))
       (is (= :graph-info (get-in result [:action :type])))))
 
-  (testing "add requires content"
-    (let [parsed {:ok? true :command :add :options {}}
+  (testing "list page requires repo"
+    (let [parsed {:ok? true :command :list-page :options {}}
+          result (commands/build-action parsed {})]
+      (is (false? (:ok? result)))
+      (is (= :missing-repo (get-in result [:error :code])))))
+
+  (testing "add block requires content"
+    (let [parsed {:ok? true :command :add-block :options {}}
           result (commands/build-action parsed {:repo "demo"})]
       (is (false? (:ok? result)))
       (is (= :missing-content (get-in result [:error :code])))))
 
-  (testing "add builds insert-blocks op"
-    (let [parsed {:ok? true :command :add :options {:content "hello"}}
+  (testing "add block builds insert-blocks op"
+    (let [parsed {:ok? true :command :add-block :options {:content "hello"}}
           result (commands/build-action parsed {:repo "demo"})]
       (is (true? (:ok? result)))
-      (is (= :add (get-in result [:action :type])))))
+      (is (= :add-block (get-in result [:action :type])))))
+
+  (testing "add page requires name"
+    (let [parsed {:ok? true :command :add-page :options {}}
+          result (commands/build-action parsed {:repo "demo"})]
+      (is (false? (:ok? result)))
+      (is (= :missing-page-name (get-in result [:error :code])))))
 
-  (testing "remove requires target"
-    (let [parsed {:ok? true :command :remove :options {}}
+  (testing "remove block requires target"
+    (let [parsed {:ok? true :command :remove-block :options {}}
           result (commands/build-action parsed {:repo "demo"})]
       (is (false? (:ok? result)))
       (is (= :missing-target (get-in result [:error :code])))))
@@ -331,8 +349,8 @@
       (is (false? (:ok? result)))
       (is (= :missing-search-text (get-in result [:error :code])))))
 
-  (testing "tree requires target"
-    (let [parsed {:ok? true :command :tree :options {}}
+  (testing "show requires target"
+    (let [parsed {:ok? true :command :show :options {}}
           result (commands/build-action parsed {:repo "demo"})]
       (is (false? (:ok? result)))
       (is (= :missing-target (get-in result [:error :code]))))))

+ 28 - 13
src/test/logseq/cli/integration_test.cljs

@@ -67,28 +67,43 @@
                      (is false (str "unexpected error: " e))
                      (done)))))))
 
-(deftest test-cli-add-search-tree-remove
+(deftest test-cli-list-add-search-show-remove
   (async done
     (let [data-dir (node-helper/create-tmp-dir "db-worker")]
       (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
                   _ (fs/writeFileSync cfg-path "{:output-format :json}")
                   _ (run-cli ["graph" "create" "--repo" "content-graph"] data-dir cfg-path)
-                  add-result (run-cli ["block" "add" "--page" "TestPage" "--content" "hello world"] data-dir cfg-path)
-                  _ (parse-json-output add-result)
-                  search-result (run-cli ["block" "search" "--text" "hello world"] data-dir cfg-path)
+                  add-page-result (run-cli ["add" "page" "--page" "TestPage"] data-dir cfg-path)
+                  add-page-payload (parse-json-output add-page-result)
+                  list-page-result (run-cli ["list" "page"] data-dir cfg-path)
+                  list-page-payload (parse-json-output list-page-result)
+                  list-tag-result (run-cli ["list" "tag"] data-dir cfg-path)
+                  list-tag-payload (parse-json-output list-tag-result)
+                  list-property-result (run-cli ["list" "property"] data-dir cfg-path)
+                  list-property-payload (parse-json-output list-property-result)
+                  add-block-result (run-cli ["add" "block" "--page" "TestPage" "--content" "hello world"] data-dir cfg-path)
+                  _ (parse-json-output add-block-result)
+                  search-result (run-cli ["search" "--text" "hello world" "--include-content"] data-dir cfg-path)
                   search-payload (parse-json-output search-result)
-                  tree-result (run-cli ["block" "tree" "--page" "TestPage" "--format" "json"] data-dir cfg-path)
-                  tree-payload (parse-json-output tree-result)
-                  block-uuid (get-in tree-payload [:data :root :children 0 :uuid])
-                  remove-result (run-cli ["block" "remove" "--block" (str block-uuid)] data-dir cfg-path)
-                  remove-payload (parse-json-output remove-result)
+                  show-result (run-cli ["show" "--page-name" "TestPage" "--format" "json"] data-dir cfg-path)
+                  show-payload (parse-json-output show-result)
+                  remove-page-result (run-cli ["remove" "page" "--page" "TestPage"] data-dir cfg-path)
+                  remove-page-payload (parse-json-output remove-page-result)
                   stop-result (run-cli ["server" "stop" "--repo" "content-graph"] data-dir cfg-path)
                   stop-payload (parse-json-output stop-result)]
-            (is (= 0 (:exit-code add-result)))
+            (is (= 0 (:exit-code add-page-result)))
+            (is (= "ok" (:status add-page-payload)))
+            (is (= "ok" (:status list-page-payload)))
+            (is (vector? (get-in list-page-payload [:data :items])))
+            (is (= "ok" (:status list-tag-payload)))
+            (is (vector? (get-in list-tag-payload [:data :items])))
+            (is (= "ok" (:status list-property-payload)))
+            (is (vector? (get-in list-property-payload [:data :items])))
             (is (= "ok" (:status search-payload)))
-            (is (seq (get-in search-payload [:data :results])))
-            (is (= "ok" (:status tree-payload)))
-            (is (= "ok" (:status remove-payload)))
+            (is (vector? (get-in search-payload [:data :results])))
+            (is (= "ok" (:status show-payload)))
+            (is (contains? (get-in show-payload [:data :root]) :uuid))
+            (is (= "ok" (:status remove-page-payload)))
             (is (= "ok" (:status stop-payload)))
             (done))
           (p/catch (fn [e]