Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

logseq-cli

A single-binary task tracker that keeps its state in a Logseq graph, so tasks stay queryable, linkable and editable in Logseq itself rather than being locked inside another tool's database.

Built on logseq-go and Kong.

Install

go build -o logseq-cli .

Setup

Point the CLI at your graph once:

logseq-cli config set graph ~/Notes

Then bind a directory to a project. This is discovered by walking up from the working directory, the way git finds .git, so it applies to subdirectories too:

cd ~/code/acme
logseq-cli init --project Acme

Project names may be namespaced. --project Acme/Clippy tracks its tasks on [[Acme/Clippy/Tasks]], nested under Acme in the graph. A nested project is fully independent of its parent: separate index page, separate task pages, separate ID sequence. Since project lookup walks up from the working directory and stops at the first .logseq-cli.toml, a subdirectory can carry its own project while the directories above it keep theirs.

Configuration lives in two places:

File Holds
~/.config/logseq-cli/config.toml graph, default_status, journal_tasks, journal_time
<project>/.logseq-cli.toml project, tags

LOGSEQ_CLI_CONFIG overrides the global config path, and LOGSEQ_CLI_LOCK_DIR overrides where lock files are kept.

Usage

logseq-cli tasks                     # open tasks (TODO, DOING, LATER)
logseq-cli tasks list --all          # including DONE and CANCELED
logseq-cli tasks list --search neh   # title or body contains "neh"
logseq-cli tasks add "Title"         # prints the new ID
logseq-cli tasks read 001
logseq-cli tasks edit 001 --title … --append … --add-tag …
logseq-cli tasks start|done|later|cancel|reopen 001
logseq-cli tasks rm 001
logseq-cli journal add "Text"        # an entry on today's journal page
logseq-cli projects                  # every project in the graph, with counts
logseq-cli doctor                    # check the index against the task pages
logseq-cli skill --install           # drop an agent guide into .claude/skills

--project is global, so any command can be pointed at a project other than the one bound to the working directory — useful from a scratch directory, or for a weekly sweep across everything:

logseq-cli --project Acme tasks list --all
logseq-cli --project Acme/Clippy doctor

Note that a project reached this way has no project file behind it, so the per-project default tags do not apply to tasks created through it.

tasks list --all-projects goes one further and lists every project's tasks in one go, for a caller with no working directory to resolve a project from:

logseq-cli tasks list --all-projects

Task IDs restart at 001 in each project, so every task carries the project it belongs to; in this listing that is what tells two 001s apart.

Every read command takes --json. --body and --append accept - to read standard input, which is the intended way to write up a long issue:

logseq-cli tasks add "Fix the NEH importer" --body - <<'EOF'
- **Context**
	- The importer assumes `updated_at` is non-null.
- **Repro**
	- `make seed && go test ./importer`
EOF

Note that an inline value beginning with - must use --body=…, since flag parsing would otherwise read it as another flag.

How tasks are stored

A project's tasks are an index page plus one page per task. For project Acme, pages/Acme___Tasks.md holds:

- DOING [[Acme/Tasks/001 Fix the NEH importer]]
  tags:: #backend
  created:: 2026-08-01
- LATER [[Acme/Tasks/002 Write the migration guide]]
  created:: 2026-08-02
- DONE [[Acme/Tasks/003 Audit the query layer]]
  created:: 2026-07-28
  completed:: 2026-08-03
  SCHEDULED: <2026-08-10 Mon>

and pages/Acme___Tasks___001 Fix the NEH importer.md holds the body.

Page titles are namespaced, so Logseq shows the hierarchy AcmeAcme/Tasks → each task, with working breadcrumbs and a namespace listing on the index page. Files in pages/ are flat with ___ separators because that is Logseq's own filename format.

The Logseq task marker lives only on the index entry, never duplicated as a property on the task page. That keeps a single source of truth and means the built-in journal queries and any TODO dashboard page pick these tasks up with no extra configuration.

Task IDs are allocated as one past the highest currently in use, scanning both the index and the pages directory so that a page left behind by rm --keep-page does not have its number reused. Deleting a task outright can free its ID for reuse; prefer cancel to keep the history.

Dates

created:: is stamped when a task is added, and completed:: when it first moves into a closed status — DONE or CANCELED. Reopening a task clears completed:: again; moving between two closed statuses keeps the date the task was first closed, so a work log does not drift forwards.

Both are plain ISO dates, which sort as strings and read well in Logseq, and both are ordinary block properties: {{query (property completed "2026-08-03")}} works, as does anything else the graph already does with properties.

Tasks written before these properties existed, or by hand in Logseq, simply have no dates. Nothing backfills them, since a guessed date is worse than an absent one.

The journal

Anything the CLI does to a task is also recorded on the day's journal page, so the journal reads as a log of what actually happened:

- **08:15** Paired with Sam on the importer
- **09:42** Added task [[Acme/Tasks/001 Fix the NEH importer]] #Acme
- **11:03** Started task [[Acme/Tasks/001 Fix the NEH importer]] #Acme
- **16:38** Completed task [[Acme/Tasks/001 Fix the NEH importer]] #Acme

Both the task and its project are linked, so the project page's linked references become an activity stream without any extra configuration. A namespaced project keeps its namespace in the tag — #Acme/Clippy — because a slash continues a Logseq hashtag rather than ending it.

An entry is written when a task is added and whenever its status changes. Re-running a status verb on a task already in that status changes nothing and writes nothing. Deleting a task is not recorded, since the page the entry would link to is gone.

The task is saved first and journalled after, on its own lock. A journal that cannot be written — the page starts with Logseq-style page properties, say — produces a warning on stderr and leaves the command successful, because the task change it describes has already happened.

Set journal_tasks to false to keep the journal to what you write yourself, or pass --no-journal to leave a single command out of it:

logseq-cli config set journal_tasks false
logseq-cli --no-journal tasks add "Not worth a line in the journal"

journal add writes an entry directly, which is the useful shape for a launcher, a shortcut or a shell alias:

logseq-cli journal add "Paired with Sam on the importer"
logseq-cli journal "Same thing, fewer keystrokes"
logseq-cli journal add "Wrote up the incident" --date 2026-08-01 --time 09:00

Entries take the same input as a task body: an indented outline nests, and plain prose becomes one block per paragraph. Piping on stdin avoids the flag parsing problem that a leading - otherwise causes:

logseq-cli journal add - <<'EOF'
- Reviewed the migration plan
	- Split the backfill out
	- Ship the read path first
EOF

Entries are timestamped and filed among the day's existing ones by time, so they stay in order however they arrive — including entries written in Logseq itself, and entries backdated with --time. journal_time chooses the format: 24h (the default), 12h, or off for no timestamps at all. Changing it does not rewrite existing entries, and entries stamped in the old format sort as if they had no timestamp, so it is worth settling on one.

Checking a project

logseq-cli doctor compares the index page against the task pages on disk and reports what it cannot reconcile:

Kind Meaning
missing_page the index links to a page that is not on disk
orphan_page a task page with no index entry, as rm --keep-page leaves
duplicate_id two index entries claiming one ID; commands resolve the first
unwritable_page a page starting with page properties, which the CLI refuses to rewrite
untracked_entry a block with a task marker that does not link to a task page

It only reads, and exits zero whether or not it finds anything — a finding is not a failed command, and most of these have more than one sensible fix. Run it after editing tasks by hand in Logseq, or when a command reports something that does not match what you see in the graph.

Raycast

raycast/ holds a personal Raycast extension built on this CLI: a no-view command for capturing a journal entry, a searchable list of every project's tasks, and a form for filing one. It shells out to the binary and parses --json rather than touching the graph itself. See raycast/README.md.

Concurrency

Several invocations may run at once — from a shell, a script, or multiple agents — against the same graph:

  • mutations take an exclusive advisory lock, keyed on the page they serialise on — the project's index page, or the day's journal page — and stored outside the graph so no lock files sync to your other machines;
  • reads take a shared lock, since pages are not written atomically;
  • ID allocation happens under the exclusive lock, so concurrent add calls always receive distinct IDs;
  • if Logseq or file sync changes a page underneath a mutation, the save is retried rather than clobbering the change.

The graph is opened without logseq-go's search index. Nothing here needs search, and skipping it avoids both a full walk of the graph on every invocation and the single-writer lock an on-disk index would impose on parallel runs.

Known limitations of the underlying library

  • Pages with Logseq-style page properties cannot be rewritten. Given a file beginning with bare key:: value lines, logseq-go re-parents every following top-level block underneath the properties block, collapsing the outline. The CLI therefore writes no page properties of its own, and refuses to write any page that starts with them rather than corrupting it. If you add page properties to a task page in Logseq, logseq-cli will decline to touch that page until they are moved into a leading bullet.
  • Writes are not atomic. Pages are written with a plain truncate-and-write, so a reader without the shared lock — Logseq itself, for instance — could in principle observe a partial file.
  • SCHEDULED: is not modelled, so the CLI treats it as raw block text. It round-trips correctly, including across status changes.

Tests

go test ./...

The end-to-end tests build the binary and run it against a throwaway graph in a temp directory, including cases that exercise a dozen concurrent processes. No test reads or writes a real graph or the real config file.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages