enhancement: add offline CLI search and predefined query

to address https://github.com/logseq/logseq/pull/12000#issuecomment-3177406045.
Also bump version for initial release
This commit is contained in:
Gabriel Horner
2025-08-13 10:42:14 -04:00
parent afe9db73a9
commit cc1093ac85
6 changed files with 74 additions and 28 deletions

11
deps/cli/README.md vendored
View File

@@ -12,7 +12,7 @@ This section assumes you have installed the CLI from npm or via the [dev
setup](#setup). If you haven't, substitute `node cli.mjs` for `logseq` e.g.
`node.cli.mjs -h`.
All commands excepts for `search` can be used offline or on CI. The `search` command and any command that has an api-query-token option require the [HTTP Server](https://docs.logseq.com/#/page/local%20http%20server) to be turned on.
All commands can be used offline or on CI. The `search` command and any command that has an api-query-token option require the [HTTP API Server](https://docs.logseq.com/#/page/local%20http%20server) to be turned on.
Now let's use it!
@@ -54,13 +54,20 @@ $ logseq show db-test
| Graph created by commit | https://github.com/logseq/logseq/commit/3c93fd2637 |
| Graph imported by | :cli/create-graph |
# Search your current graph and print results one per line like grep
# Search your current graph and print highlighted results one per line like grep
$ logseq search woot -a my-token
Search found 100 results:
dev:db-export woot woot.edn && dev:db-create woot2 woot.edn
dev:db-diff woot woot2
...
# Search a local graph
$ logseq search woot page
Search found 23 results:
Node page
Annotation page
...
# Query a graph locally using `d/entity` id(s) like an integer or a :db/ident
# Can also specify a uuid string to fetch an entity
$ logseq query woot 10 :logseq.class/Tag

View File

@@ -1,6 +1,6 @@
{
"name": "@logseq/cli",
"version": "0.1.0-alpha.1",
"version": "0.1.0",
"description": "Logseq CLI",
"bin": {
"logseq": "cli.mjs"

View File

@@ -69,12 +69,12 @@
:args->opts [:graphs] :coerce {:graphs []} :require [:graphs]}
{:cmds ["search"]
:fn (lazy-load-fn 'logseq.cli.commands.search/search)
:desc "Search current DB graph"
:args->opts [:search-terms] :coerce {:search-terms []} :require [:search-terms]
:desc "Search DB graph"
:args->opts [:graph :search-terms] :coerce {:search-terms []} :require [:graph]
:spec cli-spec/search}
{:cmds ["query"] :desc "Query DB graph(s)"
:fn (lazy-load-fn 'logseq.cli.commands.query/query)
:args->opts [:graph :args] :coerce {:args []} :no-keyword-opts true :require [:graph :args]
:args->opts [:graph :args] :coerce {:args []} :no-keyword-opts true :require [:graph]
:spec cli-spec/query}
{:cmds ["export-edn"] :desc "Export DB graph as EDN"
:fn (lazy-load-fn 'logseq.cli.commands.export-edn/export)

View File

@@ -64,20 +64,34 @@
m)))
args))
(defn- local-datalog-query [db query*]
(let [query (into query* [:in '$ '%]) ;; assumes no :in are in queries
res (d/q query db (rules/extract-rules rules/db-query-dsl-rules))]
;; Remove nesting for most queries which just have one :find binding
(if (= 1 (count (first res))) (mapv first res) res)))
(defn- local-query
[{{:keys [graph args graphs properties-readable]} :opts}]
[{{:keys [graph args graphs properties-readable title-query]} :opts}]
(let [graphs' (into [graph] graphs)]
(doseq [graph' graphs']
(if (fs/existsSync (cli-util/get-graph-dir graph'))
(let [conn (apply sqlite-cli/open-db! (cli-util/->open-db-args graph))
query* (when (string? (first args)) (common-util/safe-read-string {:log-error? false} (first args)))
;; If datalog query detected run it or else default to entity lookups
results (if (and (vector? query*) (= :find (first query*)))
;; assumes no :in are in queries
(let [query' (into query* [:in '$ '%])
res (d/q query' @conn (rules/extract-rules rules/db-query-dsl-rules))]
results (cond
;; Run datalog query if detected
(and (vector? query*) (= :find (first query*)))
(local-datalog-query @conn query*)
;; Runs predefined title query. Predefined queries could better off in a separate command
;; since they could be more powerful and have different args than query command
title-query
(let [query '[:find (pull ?b [*])
:in $ % ?search-term
:where (block-content ?b ?search-term)]
res (d/q query @conn (rules/extract-rules rules/db-query-dsl-rules)
(string/join " " args))]
;; Remove nesting for most queries which just have one :find binding
(if (= 1 (count (first res))) (mapv first res) res))
:else
(local-entities-query @conn properties-readable args))]
(when (> (count graphs') 1)
(println "Results for graph" (pr-str graph')))

View File

@@ -1,9 +1,12 @@
(ns logseq.cli.commands.search
"Search command"
(:require [clojure.pprint :as pprint]
(:require ["fs" :as fs]
[clojure.pprint :as pprint]
[clojure.string :as string]
[logseq.cli.util :as cli-util]
[datascript.core :as d]
[logseq.cli.text-util :as cli-text-util]
[logseq.cli.util :as cli-util]
[logseq.db.common.sqlite-cli :as sqlite-cli]
[promesa.core :as p]))
(defn- highlight
@@ -23,21 +26,42 @@
(recur new-result)
new-result)))))
(defn search
[{{:keys [search-terms api-query-token raw limit]} :opts}]
(-> (p/let [resp (cli-util/api-fetch api-query-token
"logseq.app.search"
[(string/join " " search-terms) {:limit limit}])]
(defn- format-results
"Results are a list of strings. Handles highlighting search term in results and printing options like :raw"
[results search-term {:keys [raw api?]}]
(println "Search found" (count results) "results:")
(if raw
(pprint/pprint results)
(let [highlight-fn (if api?
highlight-content-query
#(string/replace % search-term (highlight search-term)))]
(println (string/join "\n"
(->> results
(map #(string/replace % "\n" "\\\\n"))
(map highlight-fn)))))))
(defn- api-search
[search-term {{:keys [api-query-token raw limit]} :opts}]
(-> (p/let [resp (cli-util/api-fetch api-query-token "logseq.app.search" [search-term {:limit limit}])]
(if (= 200 (.-status resp))
(p/let [body (.json resp)]
(let [{:keys [blocks]} (js->clj body :keywordize-keys true)]
(println "Search found" (count blocks) "results:")
(if raw
(pprint/pprint blocks)
(println (string/join "\n"
(->> blocks
(map :block/title)
(map #(string/replace % "\n" "\\\\n"))
(map highlight-content-query)))))))
(format-results (map :block/title blocks) search-term {:raw raw :api? true})))
(cli-util/api-handle-error-response resp)))
(p/catch cli-util/command-catch-handler)))
(defn- local-search [search-term {{:keys [graph raw limit]} :opts}]
(if (fs/existsSync (cli-util/get-graph-dir graph))
(let [conn (apply sqlite-cli/open-db! (cli-util/->open-db-args graph))
nodes (->> (d/datoms @conn :aevt :block/title)
(filter (fn [datom]
(string/includes? (:v datom) search-term)))
(take limit)
(map :v))]
(format-results nodes search-term {:raw raw}))
(cli-util/error "Graph" (pr-str graph) "does not exist")))
(defn search [{{:keys [graph search-terms api-query-token]} :opts :as m}]
(if api-query-token
(api-search (string/join " " (into [graph] search-terms)) m)
(local-search (string/join " " search-terms) m)))

View File

@@ -28,12 +28,13 @@
:properties-readable {:alias :p
:coerce :boolean
:desc "Make properties on entity queries show property values instead of ids"}
:title-query {:alias :t
:desc "Invokes local query on :block/title"}
:api-query-token {:alias :a
:desc "Query current graph with api server token"}})
(def search
{:api-query-token {:alias :a
:require true
:desc "Api server token"}
:raw {:alias :r
:desc "Print raw response"}