Skip to content

About

The Johnny.Decimal command-line tool

Resources

Stars

11 stars

Watchers

2 watching

Forks

Repository files navigation

The JD CLI utilities

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.

Requirements

Installation

  1. Install jq if it is not installed. On macOS: brew install jq.2

  2. Clone this repository to ~/.local/share/johnnydecimal/cli:

    git clone https://github.com/johnnydecimal/cli.johnnydecimal.com \
      ~/.local/share/johnnydecimal/cli
  3. If ~/.config/johnnydecimal/config.json does 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_cli tool returns the steps to an agent: clone this repository, then run this command:

    ~/.local/share/johnnydecimal/cli/bin/jd agent-setup

    On macOS, add | pbcopy to 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
  4. Add this line to .zshrc, or to .bashrc:

    source ~/.local/share/johnnydecimal/cli/jd.sh

    In fish, add this line to config.fish instead:

    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 whose sys is already a command name, such as cd, and prints an error. In zsh and bash it also defines _jd_pwd, the prompt path.

  5. For the prompt, add one of these two to .zshrc or .bashrc, after the source line. There is no prompt for fish.

    A prompt of your own, with the Johnny.Decimal path in it. In zsh, _jd_pwd requires single quotes, or prompt_subst:

    setopt prompt_subst
    PROMPT='%B$(_jd_pwd)%b %# '

    In bash, _jd_pwd requires single quotes:

    PS1='\[\e[1m\]$(_jd_pwd)\[\e[0m\] \$ '

    Or Johnny's prompt. It sets PROMPT in zsh and PS1 in 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
  6. Start a new shell.

Running jd from a script or an agent

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.

  • --system is read as the first word and nowhere else, so it never collides with a title or with a flag of jd new.

  • jd new takes --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 move and jd undo move take --json too. Refer to jd move.

  • jd paths prints the path of the configuration file, the path of the journal, and the folder that contains the program. jd paths --json prints one JSON object. A script or an agent that requires one of these paths must read the path from jd paths. Refer to Where jd stores its files.

  • The source line in Installation also adds bin to $PATH, so a program started from your shell can run jd by name.

  • If ~/.local/bin is 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

The prompt theme

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.zsh

Or 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 own gitprompt function prints nothing.

  • In bash, the theme adds _jd_theme_rc=$? to the start of PROMPT_COMMAND. That is how the prompt reads the exit status of the last command.

  • The symbols require a font that contains box-drawing characters.

Where jd stores its files

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_CONFIG is 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 source line.
  • 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 object
config   /Users/you/.config/johnnydecimal/config.json
journal  /Users/you/.local/state/johnnydecimal/journal.jsonl
install  /Users/you/.local/share/johnnydecimal/cli
  • install is the folder that contains this copy of jd.
  • If the configuration file or the journal is still in ~/.jd, jd paths prints the path in ~/.jd, because jd uses that file. jd paths also prints, on stderr, the commands that move the file. Refer to Move from ~/.jd.
  • The object from jd paths --json has a warnings list. 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.

Configuration

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 jdex is an object. app is the app that contains the JDex, and path is its folder, if it is on disk.
  • workPackages.notes is gone. The note app is jdex.app.
  • workPackages.tasks.adapter is workPackages.tasks.app.
"jdex": { "app": "obsidian", "path": "/Users/you/Documents/D25 JDex", "vault": "D25 JDex" }
"jdex": { "app": "bear" }
"jdex": { "path": "/Users/you/Documents/P76 JDex" }

The 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.11 opens the note in Bear, and jdex 11 opens the tag. Neither changes the directory.
  • jd new writes 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, set JD_BEARCLI to its path.

jd new

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 --help or jd new wp --help for the full list of flags.

jd new id

  • 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 21 and 21.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.

jd new wp

  • 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-9999 folder, jd creates W0000-9999 Work packages, with W0003 Templates for area W0000-9999 and W0009 Archive in it.
  • If the area has no W0003 folder, jd creates W0003 Templates for area W0000-9999, and copies its own template note into it: templates/Work package template.md.
  • If the JDex has no W0000-9999 folder, jd creates W0000-9999 Work packages in 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 is workPackages.tasks.app. Refer to lib/adapters/README.md.

Templates

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.md that 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 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 toFill with --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.
  • The Things template, with workPackages.tasks.source in 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 --refresh does the same, and creates nothing.
    • workPackages.tasks.source in the config is the project's ID in Things.
  • The Things template, with no workPackages.tasks.source: you edit the file in W0003.
    • If W0003 has no file, jd copies its own into W0003.
    • jd does not write to the file again.

For agents

  • --json prints 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.
    • code is the stable part, and it is never empty. Match on it. The message is written for a person to read, so it may be reworded.
    • warnings lists the codes for problems that did not cause the command to exit, for example no_template.
  • To fill in the tokens, first run a dry run with --json. toFill lists each token with its brief and its flag. Then run the command again with a value for each flag.
  • --dry-run prints what jd would create, and which templates it would use.

jd move

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.34 finds 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 in from and the new name in to. 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.icloud file 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 $f ends 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. cd out of it first.
  • --dry-run prints what it would do, and moves nothing.
  • Run jd move --help or jd undo move --help for the full text.

The journal

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"}
  • by is person when the shell ran it, and program when a script or an agent ran the program.
  • An undo line has undoes, the at of the move it undid, and from and to the 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.

jd undo move

  • With no path, it undoes the newest move in the system that is not yet undone. --system selects 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, code source_exists.
  • An undo is never a candidate for undo.

Beta

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 off

The flag is "beta": true at the top level of the configuration file. JD_BETA=1 turns beta on for one shell, and writes nothing.

Update

jd update

jd 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 pull

The update check

Once 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": false at the top level of the configuration file.
  • Set JD_NO_UPDATE_CHECK=1.
  • Set DO_NOT_TRACK=1.

Move from ~/.jd

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 ~/.jd prints one line that indicates this.
  • A later version of jd will not read ~/.jd.

To move the files:

  1. Update the copy you have:

    git -C ~/.jd/cli pull
  2. 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-setup

    Or 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/
  3. In your shell config, change ~/.jd/cli to ~/.local/share/johnnydecimal/cli in each source line. Then start a new shell.

  4. When ~/.jd is 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 ~/.jd and 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.

From 1.x

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.

Tests

test/run.sh

The 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

  • 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 version prints the installed version, and the newer version if the update check found one.
  • CHANGELOG.md lists the changes in each release.

Licence

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.

AI attribution

  • 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 🫠.

Footnotes

  1. PRs welcome if you do. ↩

  2. Requires Homebrew. ↩

About

The Johnny.Decimal command-line tool

Resources

Stars

11 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages