Written by Claude. See AI attribution.
Command-line tools for a Johnny.Decimal system. They operate in bash 3.2 and later, in zsh, and in fish 3.5 and later. The code is MIT licensed. Adapt it for other shells as necessary.1
This repository documents installation. Usage is documented at johnnydecimal.com/documentation/jd-cli.
- Configuration file.
- This repository contains a template.
- jq.
-
Install
jqif it is not installed. On macOS:brew install jq.2 -
Clone this repository to
~/.local/share/johnnydecimal/cli:git clone https://github.com/johnnydecimal/cli.johnnydecimal.com \ ~/.local/share/johnnydecimal/cli -
If
~/.config/johnnydecimal/config.jsondoes not exist, create it. Either way works.Let your agent write it. This prints a prompt. Paste it into whatever agent you use, and it finds your systems, writes the file, adds the source line, and offers the prompt. It is the one prompt for setup. With the johnnydecimal.com MCP server, the
install_clitool returns the steps to an agent: clone this repository, then run this command:~/.local/share/johnnydecimal/cli/bin/jd agent-setupOn macOS, add
| pbcopyto that line to copy the prompt to the clipboard.Or copy the template and edit it for your systems:
mkdir -p -m 700 ~/.config/johnnydecimal cp ~/.local/share/johnnydecimal/cli/config.example.json \ ~/.config/johnnydecimal/config.json
-
Add this line to
.zshrc, or to.bashrc:source ~/.local/share/johnnydecimal/cli/jd.sh
In fish, add this line to
config.fishinstead:source ~/.local/share/johnnydecimal/cli/jd.fish
It defines
jd,jdex, and one command per system in your configuration. Each one runs the program and changes to the folder that the program prints. It defines no command for a system whosesysis already a command name, such ascd, and prints an error. In zsh and bash it also defines_jd_pwd, the prompt path. -
For the prompt, add one of these two to
.zshrcor.bashrc, after thesourceline. There is no prompt for fish.A prompt of your own, with the Johnny.Decimal path in it. In zsh,
_jd_pwdrequires single quotes, orprompt_subst:setopt prompt_subst PROMPT='%B$(_jd_pwd)%b %# 'In bash,
_jd_pwdrequires single quotes:PS1='\[\e[1m\]$(_jd_pwd)\[\e[0m\] \$ 'Or Johnny's prompt. It sets
PROMPTin zsh andPS1in bash, so put the line after all other lines that set the prompt. Refer to The prompt theme:source ~/.local/share/johnnydecimal/cli/lib/theme.zsh
source ~/.local/share/johnnydecimal/cli/lib/theme.bash
-
Start a new shell.
- These are the default paths. If you set
$XDG_DATA_HOMEor$XDG_CONFIG_HOME, refer to Where jd stores its files. - If you installed jd before 4.0.0, your files are in
~/.jd. Refer to Move from~/.jd.
The tool is a program, ~/.local/share/johnnydecimal/cli/bin/jd. Nothing has to be sourced to run it, so a script, cron, or an agent's shell can use it.
~/.local/share/johnnydecimal/cli/bin/jd 11.11 # the folder for ID 11.11
~/.local/share/johnnydecimal/cli/bin/jd --system P76 22 # a system other than the default
~/.local/share/johnnydecimal/cli/bin/jd version
~/.local/share/johnnydecimal/cli/bin/jd paths # where jd stores its files-
A move prints one absolute path on stdout and exits 0, so a command substitution can use it:
cd "$(~/.local/share/johnnydecimal/cli/bin/jd 11.11)". -
jd prints match lists, reports, and errors on stderr.
-
--systemis read as the first word and nowhere else, so it never collides with a title or with a flag ofjd new. -
jd newtakes--json, which prints one JSON object on stdout. Refer to For agents.~/.local/share/johnnydecimal/cli/bin/jd new id 21 A title --json -
jd moveandjd undo movetake--jsontoo. Refer tojd move. -
jd pathsprints the path of the configuration file, the path of the journal, and the folder that contains the program.jd paths --jsonprints one JSON object. A script or an agent that requires one of these paths must read the path fromjd paths. Refer to Where jd stores its files. -
The
sourceline in Installation also addsbinto$PATH, so a program started from your shell can runjdby name. -
If
~/.local/binis on your$PATH, you can create a link to the program there. The program follows the link to find its other files:ln -s ~/.local/share/johnnydecimal/cli/bin/jd ~/.local/bin/jd
lib/theme.zsh is Johnny's prompt, and lib/theme.bash is the same prompt for bash. jd.sh does not load either.
┏╸D25:…/11.11 Structure & registrations
┗╸mymac ❯❯
Add this line to .zshrc, after all other lines that set PROMPT:
source ~/.local/share/johnnydecimal/cli/lib/theme.zshOr add this line to .bashrc, after all other lines that set PS1:
source ~/.local/share/johnnydecimal/cli/lib/theme.bash-
Chevron colours. The prompt reads these variables at each prompt, so put them before or after the source line:
JD_CHEVRON1=yellow JD_CHEVRON2=red
In bash, a colour is one of
black,red,green,yellow,blue,magenta,cyan,white,default, or a number from 0 to 255. -
For git status in zsh, load git-prompt.zsh before the theme. In bash, load git's own git-prompt.sh, before or after the theme. The theme calls its
__git_ps1. If neither is loaded, the theme's owngitpromptfunction prints nothing. -
In bash, the theme adds
_jd_theme_rc=$?to the start ofPROMPT_COMMAND. That is how the prompt reads the exit status of the last command. -
The symbols require a font that contains box-drawing characters.
jd follows the XDG Base Directory Specification. In each base directory, jd uses a folder with the name johnnydecimal.
| What | Default path | With the variable set |
|---|---|---|
| The configuration file | ~/.config/johnnydecimal/config.json |
$XDG_CONFIG_HOME/johnnydecimal/config.json |
| The journal | ~/.local/state/johnnydecimal/journal.jsonl |
$XDG_STATE_HOME/johnnydecimal/journal.jsonl |
| This repository | ~/.local/share/johnnydecimal/cli |
$XDG_DATA_HOME/johnnydecimal/cli |
| The update check | ~/.cache/johnnydecimal/update-check |
$XDG_CACHE_HOME/johnnydecimal/update-check |
$JD_CONFIGis the path of the configuration file. If you set$JD_CONFIG, jd reads only that file.- A path in an XDG variable must start with
/. If the path does not start with/, jd uses the default path. - If you set
$XDG_CONFIG_HOME, jd searches only that directory. jd does not also search~/.config. - If the folder for the journal does not exist, jd creates the folder with mode 700.
- You can clone this repository to any folder. The program finds its other files from its own path. The third row is only the folder that these instructions use. If you use a different folder, put that folder in the
sourceline. - The paths are the same on macOS. jd is a command-line tool, so it does not use
~/Library.
jd paths prints the paths that jd uses on this machine. jd paths does not require a configuration file or jq. Only jd paths --json requires jq.
jd paths # all three
jd paths config # one path and nothing else, for a script
jd paths --json # one JSON objectconfig /Users/you/.config/johnnydecimal/config.json
journal /Users/you/.local/state/johnnydecimal/journal.jsonl
install /Users/you/.local/share/johnnydecimal/cli
installis the folder that contains this copy of jd.- If the configuration file or the journal is still in
~/.jd,jd pathsprints the path in~/.jd, because jd uses that file.jd pathsalso prints, on stderr, the commands that move the file. Refer to Move from~/.jd. - The object from
jd paths --jsonhas awarningslist. Each code in the list indicates the state of one item in~/.jd:old_config: jd reads the configuration file in~/.jd.old_journal: jd uses the journal in~/.jd.old_install: this copy of jd is in~/.jd/cli.old_config_ignored: there is a configuration file in~/.jd, but jd reads the file in the new place.old_journal_ignored: there is a journal in~/.jd, but jd uses the journal in the new place.
All the tools read one configuration file. Refer to Where jd stores its files for its path.
The program reads the configuration each time it runs, so a system that has moved takes effect at once.
Two things are read at shell start, and require a new shell: the list of per-system commands, because a shell cannot be given a new command name later, and the prompt path.
Refer to johnnydecimal.com/documentation/configuration for the fields. config.example.json is an example.
jd reads version 2 of the configuration file. The first command that reads a version 1 file changes the file to version 2, and keeps the old file next to it as config.json.v1. In version 2:
- A system's
jdexis an object.appis the app that contains the JDex, andpathis its folder, if it is on disk. workPackages.notesis gone. The note app isjdex.app.workPackages.tasks.adapterisworkPackages.tasks.app.
"jdex": { "app": "obsidian", "path": "/Users/you/Documents/D25 JDex", "vault": "D25 JDex" }
"jdex": { "app": "bear" }
"jdex": { "path": "/Users/you/Documents/P76 JDex" }jd reads a JDex in one of three layouts.
- Nested: a folder of area folders, which contain category folders, which contain the notes.
10-19 Life admin/11 Me/11.11 Birth certificate.md. - Flat: a folder with no area folders. Every note is in its root.
11.11 Birth certificate.md. - Bear:
"jdex": { "app": "bear" }. An area is a tag, such as#10-19 Life admin#, and a category is a tag nested below it, such as#10-19 Life admin/11 Me#. An ID is a note with its category's tag, whose title starts with the ID. A work package is a note with the work package area's tag,#W0000-9999 Work packages#.
In Bear:
- jd matches each tag by the number at its start, the same way it matches a folder, so the rest of the name can be anything.
- A tag nested below a category counts as the category.
- A note in the trash is not in the JDex. An archived note is.
- If one Bear library contains more than one system, put the tags of each system below a top-level tag that starts with its
sys, such as#P76 Personal#. jd reads that system's tags there. jdex 11.11opens the note in Bear, andjdex 11opens the tag. Neither changes the directory.jd newwrites the note with its title as the first line and its tag as the second.- jd runs Bear's command line tool,
/Applications/Bear.app/Contents/MacOS/bearcli. If yours is somewhere else, setJD_BEARCLIto its path.
jd new creates a new thing. The word after new specifies what to create. It is required.
The title does not require quotes. It is every word after the category or ID, up to the first word that starts with --. A - anywhere else is part of the title, for example SBS video - 14.20 Syncthing or Cut costs -20%. You can put flags before the noun, after it, or after the title.
jd new id 21 A title # the next free ID in category 21
jd new id 21.34 A title # exactly ID 21.34, if it is not used
jd new wp 21.34 A title # a work package for ID 21.34- Run
jd new id --helporjd new wp --helpfor the full list of flags.
- It creates the JDex note and the folder.
- The next free ID is one more than the highest ID in the category.
- It counts the IDs in the JDex: the entries directly in the category, and directly in its archive, the .09 ID. For category 21, that is
21and21.09. - In a flat JDex, jd counts the notes in the root that start with the category's number, and writes the new note to the root.
- In Bear, jd counts the notes with the category's tag, and the tags nested below it.
- An ID in the JDex can be a folder that contains its own note, with exactly the folder's name:
21.41 Videos/21.41 Videos.md. This matches how the Obsidian 'Folder notes' plugin works. jd reads an ID like this, and creates every new ID as a note. - A folder with no JDex entry is not an ID, so it is not counted. Nor is anything deeper inside an ID.
- If the filesystem already has a folder with the new ID's number, jd exits with an error that names the folder.
- It is never lower than .11.
- A gap is not filled.
- It counts the IDs in the JDex: the entries directly in the category, and directly in its archive, the .09 ID. For category 21, that is
- It assigns the next free W number, and creates the project in your task app, the JDex note, and the folder.
- The next free W number is one more than the highest in the JDex: the entries directly in the work package area, and directly in its archive,
W0009. If the filesystem already has a folder with that number, jd exits with an error that names the folder. - If the system has no
W0000-9999folder, jd createsW0000-9999 Work packages, withW0003 Templates for area W0000-9999andW0009 Archivein it. - If the area has no
W0003folder, jd createsW0003 Templates for area W0000-9999, and copies its own template note into it: templates/Work package template.md. - If the JDex has no
W0000-9999folder, jd createsW0000-9999 Work packagesin the JDex. In a flat JDex, jd creates no folder: jd writes the note to the root. In Bear, the note's tag creates the tag#W0000-9999 Work packages#. - The note app is the JDex's
app, and the task app isworkPackages.tasks.app. Refer to lib/adapters/README.md.
Templates are in the filesystem. jd finds them by number, so the config does not name them.
| Template | Where jd searches | If there is none |
|---|---|---|
ID template.md |
The category's .03, then the area's .03, then 00.03. For category 21: 21.03, 20.03, 00.03. |
The note is blank, and jd prints a warning. |
Work package template.md |
W0003 |
The note is blank, and jd prints a warning. If there is no W0003, jd creates it, and copies its own template note into it. |
Work package template/ |
W0003. jd copies its contents into the new folder. |
The folder is empty. |
Work package template.things.json |
W0003 |
jd creates it from Things, if the config has workPackages.tasks.source. Otherwise jd copies its own, templates/Work package template.things.json, into W0003. |
- The first
ID template.mdthat jd finds is the one it uses. To give one category its own template, put a copy in that category's .03. - If there are two folders with the same templates number, jd exits with an error. jd cannot determine which folder to use.
- The note never contains
{{.{{ID}},{{TITLE}}and{{NAME}}are filled in. A work package also has{{W}},{{w}},{{NOTES_URL}},{{TASKS_URL}}and{{PARENT_LINK}}.- Anything else in double braces that jd does not recognize is left out of the note, and jd prints a warning.
- A line that contains
{{NOTES_URL}}or{{TASKS_URL}}is left out when there is no such link: no adapter is set, or you gave--no-tasks. This applies to the note and to the Things template. - A line with lines indented under it is left out when all of those lines are left out. So
- Related:is left out when it has no link left under it. {{PARENT_LINK}}is the note app's link to the ID's own JDex note. In Obsidian and in Bear it is a wiki-link, for example[[21.41 Videos]], so the app lists the work package in the backlinks of the ID's note. jd does not write to the ID's note.- A line that contains
{{PARENT_LINK}}is left out when the JDex has no app. - The ID's note can be in a folder with the same name:
21.41 Videos/21.41 Videos.md. - It is also left out when the JDex has no note for the ID, or more than one. jd prints a warning, with the code
no_parent_note.
- A line that contains
- A template note can contain a token, for example
{{?SCOPE}}or{{?DELIVERABLE What we hand over}}.- Its flag is its name in lower case, with
-for_:--scope,--deliverable. - Give the value after the title:
jd new wp 21.41 A title --scope "What we do" --deliverable=Video. - A token with no value becomes its brief,
What we hand over, or nothing. - jd prints the name of each flag you did not set: in
toFillwith--json, or on stderr otherwise. - A flag for a token that the template does not have is an error. The error lists the flags the template has.
- Its flag is its name in lower case, with
- The Things template, with
workPackages.tasks.sourcein the config: you edit the template project in Things.- Before jd creates each work package, jd reads that project from Things. If the project has changed, jd rewrites the file.
jd new wp --refreshdoes the same, and creates nothing.workPackages.tasks.sourcein the config is the project's ID in Things.
- The Things template, with no
workPackages.tasks.source: you edit the file inW0003.- If
W0003has no file, jd copies its own intoW0003. - jd does not write to the file again.
- If
--jsonprints one JSON object on stdout, in place of the usual lines.{ "ok": true, ... }on success,{ "ok": false, "code": "...", "message": "...", "path": "..." }on error. jd still prints the lines for a person on stderr.codeis the stable part, and it is never empty. Match on it. Themessageis written for a person to read, so it may be reworded.warningslists the codes for problems that did not cause the command to exit, for exampleno_template.
- To fill in the tokens, first run a dry run with
--json.toFilllists each token with itsbriefand itsflag. Then run the command again with a value for each flag. --dry-runprints what jd would create, and which templates it would use.
jd move moves a file or a folder into the folder for an ID, and writes one line to a journal. jd undo move reads the journal and moves it back.
jd move ~/Downloads/invoice.pdf 21.34 # into the folder for 21.34
jd move ~/Desktop/Photos W0189 # a folder moves whole
jd move ~/Downloads/x.pdf "21.34/Bank statements" # into a subfolder
jd move ~/Downloads/x.pdf 21.34 --as "2024-03-14 x.pdf" # with a new name
jd undo move # move the last thing back
jd undo move ~/D25/.../invoice.pdf # move that thing back- The target is an ID or a W number. It is found the same way
jd 21.34finds it. If an ID is in the JDex but has no folder, jd creates its folder. <id>/<subfolder>moves the file or folder into a subfolder of the ID's folder, one level down. jd creates the subfolder when it does not exist. An undo leaves the subfolder in place.--as <name>renames the file or folder as jd moves it. The journal stores the old name infromand the new name into. An undo restores the old name.- It prints the new path on stdout and exits 0. It does not cd.
- It exits with an error, and moves nothing, when the target folder already contains something with that name. With
--as, the check is on the new name. - It rejects an iCloud stub, a
.name.icloudfile that is not downloaded. A Dropbox online-only file is not detected. - It exits with an error when the target folder is inside the thing you are moving.
- It rejects a path that ends in
/.Photos/means something in Photos, not Photos itself."$PWD/$f"with an empty$fends in/this way. To move the folder, leave the/off. - It exits with an error when you are in the folder you are moving, or in a folder inside it.
cdout of it first. --dry-runprints what it would do, and moves nothing.- Run
jd move --helporjd undo move --helpfor the full text.
Every move is one line in the journal, ~/.local/state/johnnydecimal/journal.jsonl. So is every undo. The file is append only, and jd never edits it. jd paths journal prints the path of the journal on this machine.
{"at":"2026-09-12T07:09:53Z","sys":"D25","id":"21.34","kind":"file","from":"/Users/you/Downloads/invoice.pdf","to":"/Users/you/Documents/D25/20-29 Finance/21 Accounts/21.34 Invoices/invoice.pdf","by":"person","host":"mymac.local"}byispersonwhen the shell ran it, andprogramwhen a script or an agent ran the program.- An undo line has
undoes, theatof the move it undid, andfromandtothe other way round. - The journal is for this machine. The paths in it belong here, so it is not in the system, and it does not sync. For this reason, jd stores the journal in the XDG state directory. Refer to Where jd stores its files.
- jd writes nothing into the JDex. An agent that files a mess reads the JSON result and writes the note itself.
- With no path, it undoes the newest move in the system that is not yet undone.
--systemselects the system. - With a path, it undoes the move that moved the file or folder to that path, in whichever system.
- It exits with an error if the moved thing is no longer where jd moved it, code
moved_since, or if something else is now at its original path, codesource_exists. - An undo is never a candidate for undo.
Some features are in beta. They may change, or be removed, without notice. One flag turns all of them on. No feature is in beta in this version.
jd beta # print whether beta is on
jd beta on # turn it on
jd beta off # turn it offThe flag is "beta": true at the top level of the configuration file. JD_BETA=1 turns beta on for one shell, and writes nothing.
jd updatejd update pulls the latest version, and prints the changelog entries since the version you had. The folder that contains jd must be on the branch main, with no uncommitted changes.
If your version of jd has no jd update, run:
git -C ~/.local/share/johnnydecimal/cli pullOnce a day, jd checks GitHub for a newer version. If there is one, jd prints one line after each command. The check reads the list of tags from GitHub. No telemetry is sent.
jd checks only when it prints to a terminal, so a script, cron, or an agent never starts a check. To turn the check off, do one of these:
- Add
"updateCheck": falseat the top level of the configuration file. - Set
JD_NO_UPDATE_CHECK=1. - Set
DO_NOT_TRACK=1.
Before 4.0.0, jd stored all its files in ~/.jd: the configuration file, the journal, and this repository, which was in ~/.jd/cli. From 4.0.0, jd uses the paths in Where jd stores its files.
jd continues to operate from ~/.jd after you update it, and you do not have to move the files immediately.
- If the new place has no configuration file, jd reads the configuration file in
~/.jd. - If the new place has no journal, jd uses the journal in
~/.jd. - Each command that uses a file in
~/.jdprints one line that indicates this. - A later version of jd will not read
~/.jd.
To move the files:
-
Update the copy you have:
git -C ~/.jd/cli pull -
Move the files. Either way works.
Your agent can move the files. This command prints a prompt for your agent. The first step in the prompt instructs the agent to move the files:
~/.jd/cli/bin/jd agent-setupOr move the files yourself. Run
~/.jd/cli/bin/jd paths. It prints the commands that move the files on this machine. It prints commands only for the items that are still in~/.jd. If you set no XDG variable, the commands are:mkdir -p -m 700 ~/.config/johnnydecimal mv ~/.jd/config.json ~/.config/johnnydecimal/ mkdir -p -m 700 ~/.local/state/johnnydecimal mv ~/.jd/journal.jsonl ~/.local/state/johnnydecimal/ mkdir -p -m 700 ~/.local/share/johnnydecimal mv ~/.jd/cli ~/.local/share/johnnydecimal/
-
In your shell config, change
~/.jd/clito~/.local/share/johnnydecimal/cliin eachsourceline. Then start a new shell. -
When
~/.jdis empty, remove it:rmdir ~/.jd.
- If you set
$JD_CONFIG, jd continues to read that file. Do not move the configuration file. - If there is a configuration file in
~/.jdand also in the new place, jd reads the file in the new place and prints a warning. Delete the file in~/.jd. - If you use the Johnny.Decimal agent skills, update the skills before you move the configuration file. An older skill searches for the configuration file in
~/.jd. - You can keep this repository in
~/.jd/cli, because jd operates from any folder. But then you cannot remove~/.jd.
The v1.x jd-nav and jd-prompt paths are deleted at 3.0.0. A .zshrc that still sources one fails at shell start with "no such file".
Replace both lines with the single line in Installation, which has been the documented line since 2.0.0.
test/run.shThe suite runs in bash and in zsh. It exits non-zero if a test fails. It
requires jq, and nothing else. It runs against fixture systems in a temporary
folder, not against your system.
jd.fish has its own coverage in test/cases/fish.sh, which runs fish
as a subprocess. It requires fish too. When there is no fish on the
machine, the case file skips itself and prints one line.
Refer to test/README.md to add a test.
- Versions follow semantic versioning.
- One version applies to all the tools in the repository.
- Each release has a git tag, for example
v2.0.0. jd versionprints the installed version, and the newer version if the update check found one.- CHANGELOG.md lists the changes in each release.
The code in this repository is MIT, copyright Coruscade Pty Ltd.
The Johnny.Decimal system, its documentation, and the name are separate. See johnnydecimal.com/licence. Johnny.Decimal is a trademark of Coruscade Pty Ltd.
- Johnny:
- Designs the utility, i.e. decides what it does and how it behaves.
- Claude:
- Writes the code.
- Writes the text in the repository, e.g. the installation instructions, which Johnny then edits to save you from reading too much Claude 🫠.