Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "km",
"version": "1.2.0",
"version": "1.3.0",
"description": "Knowledge management for markdown knowledge bases: answer from it with sources, save notes, decisions and transcripts with validated frontmatter, lint, mount peer brains, contribute to a team brain by PR, and set up a shared team brain with CI (/km init --team).",
"author": {
"name": "Roman Passler"
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
KM_REPO_URL="file://$GITHUB_WORKSPACE" /tmp/wheel/bin/km init /tmp/wheel-brain --team --name W --desc w --initials WW --folder d=D
- run: bash tests/gate_smoke.sh
- run: bash tests/index_smoke.sh
- run: bash tests/doctor_smoke.sh
- name: team_smoke.sh, with a Latin-1 locale for its file-name encoding test
run: sudo localedef -i en_US -f ISO-8859-1 en_US.ISO-8859-1 && bash tests/team_smoke.sh

Expand Down Expand Up @@ -80,6 +81,10 @@ jobs:
source tests/lib.sh
windows_like "" || { echo "::error::the runner's encoding is UTF-8, this job tests nothing"; exit 1; }
with_env "" bash tests/encoding_smoke.sh # as checked: UTF-8 mode off
- name: km doctor on Windows
env:
PYTHON: python
run: bash tests/doctor_smoke.sh

# Merging a version bump is the release: once main is green, tag v<__version__> if that tag is new.
# Brains pin these tags, and `km init` writes the tag of the km that runs it.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ brains. See README.md for the layout.
- A release bumps `src/km/__init__.py`, `.claude-plugin/plugin.json` and the README examples together
(a test checks it). Merging it to main is the release: CI tags `vX.Y.Z` once main is green. The
templates pin the tag of the km that wrote them.
- Run all three smoke tests before pushing; add a case for every behaviour change and check that it
- Run all the smoke tests before pushing; add a case for every behaviour change and check that it
fails without the change.
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ As a Claude Code plugin:
/km brain add https://github.com/org/team-brain.git # mount another brain
/km @all Zephyr RTOS # search all mounted brains
/km upgrade # move the brain to the installed km version
/km check my setup # what this computer still needs for the brain (km doctor)
/km help # all commands
```

Expand All @@ -49,12 +50,19 @@ km promote ... move a note into the brain as a review doc
km approve DOC... --by WHO sign review docs off; ai:<model> only where the doc says approval: ai
km init DIR [--team ...] create a brain from the templates
km upgrade move a brain's km pins to this version
km doctor [--offline] check this computer is set up for the brain
```

`km doctor` reads what to expect from the brain alone: its origin, the brains it mounts
(`.gitmodules`), the km version it pins, and the plugins its `.claude/settings.json` enables
(`enabledPlugins`, with the marketplaces in `extraKnownMarketplaces`). A brain lists its plugins there
for Claude Code anyway, which offers to add the marketplace when the folder is trusted. Each check
that fails names its fix; it needs no PyYAML, so it also runs on a computer that is half set up.

Every command takes `--root DIR`; the default is the git work tree around the current directory.
Install a release by its commit (`git ls-remote https://github.com/pasrom/km refs/tags/v1.2.0` shows
it), not by the tag: `pip install git+https://github.com/pasrom/km@<commit> # v1.2.0`, or run it
without installing: `uvx --from git+https://github.com/pasrom/km@<commit> km validate # v1.2.0`.
Install a release by its commit (`git ls-remote https://github.com/pasrom/km refs/tags/v1.3.0` shows
it), not by the tag: `pip install git+https://github.com/pasrom/km@<commit> # v1.3.0`, or run it
without installing: `uvx --from git+https://github.com/pasrom/km@<commit> km validate # v1.3.0`.

## What a brain gets

Expand All @@ -63,13 +71,13 @@ Brains carry no km code. `km init` writes `CONVENTIONS.md`, `CLAUDE.md`, `schema
generated indexes, a README, and CI that installs km through this repository's GitHub Action:

```yaml
- uses: pasrom/km@<commit> # v1.2.0, installs the km CLI at that commit
- uses: pasrom/km@<commit> # v1.3.0, installs the km CLI at that commit
- run: km validate
- run: km gen-index --check
```

The pre-commit hook works the same way (`repo: https://github.com/pasrom/km`,
`rev: <commit> # frozen: v1.2.0`, `id: km-validate`). Every pin names a release by its commit, so a
`rev: <commit> # frozen: v1.3.0`, `id: km-validate`). Every pin names a release by its commit, so a
tag moved later cannot change what a brain runs; the version rides along as a comment, and
`km upgrade` checks that the commit is that release's. km's own dependencies are pinned to exact
versions. `km init` and `km upgrade` look a release's commit up by its tag once, so a release tag here
Expand All @@ -83,7 +91,7 @@ a brain from the copy-in days into one that pins km.
.claude-plugin/ plugin and marketplace manifests
action.yml GitHub Action: installs km at the action's version
.pre-commit-hooks.yaml pre-commit hook km-validate
src/km/ the package: cli, validate, gen_index, demote, serve, promote, init, upgrade
src/km/ the package: cli, validate, gen_index, demote, serve, promote, init, upgrade, doctor
src/km/templates/ what `km init` writes
skills/km/SKILL.md the skill; skills/km/bin/km runs the package from the installed plugin
tests/ smoke tests
Expand All @@ -92,7 +100,7 @@ tests/ smoke tests
## Development

The smoke tests need Python 3.11+, PyYAML and git: `bash tests/gate_smoke.sh`, `index_smoke.sh`,
`team_smoke.sh`. CI runs all three plus the GitHub Action on every pull request, and
`team_smoke.sh`, `doctor_smoke.sh`. CI runs all four plus the GitHub Action on every pull request, and
`encoding_smoke.sh` on Windows (team_smoke runs it on macOS under non-UTF-8 locales). A release bumps
`__version__` in `src/km/__init__.py`, `version` in `.claude-plugin/plugin.json` and the version in
this README together; merging that to main is the release, CI tags `vX.Y.Z` once main is green.
Expand Down
21 changes: 20 additions & 1 deletion skills/km/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ description: >-
knowledge base or looked up from it, rather than writing a markdown file by hand.
Triggers on: km, knowledge base, brain, save this, was steht im brain, ins brain
speichern, dokumentiere das, Entscheidung festhalten, Meeting-Notiz, ADR, lint,
"speicher das sauber ab", "was wissen wir über".
"speicher das sauber ab", "was wissen wir über", check my setup, "prüfe meine
Einrichtung".
argument-hint: question, content, brain command, help, etc.
---

Expand All @@ -30,6 +31,24 @@ it runs that commit through `uvx`, so results match CI (`KM_LOCAL=1` keeps the p
offline, for a pin to a movable tag, or when the workflows disagree, it runs the plugin's km and warns;
`/km upgrade` the brain then.

## Setup check (`/km doctor`, "check my setup")

For someone who just set up a computer for the brain, often not a developer. Answer in their
language and in plain words, one line per problem, the fix first; no git jargon they did not use.

1. Before km: `git --version`. Without git, nothing else works: say so and stop; the brain's own
setup guide (README or CLAUDE.md) says how to install it.
2. `km doctor` in the brain (`--offline` when they say they have no network). Where `km` itself
cannot start, its message names uv and the one-line command that installs it (no administrator
rights needed): offer to run that command for their system, then run `km doctor` again. km finds
uv where the installer puts it, so the app need not be restarted first.
3. Each `FAIL` comes with a fix. Offer to run the ones that change only this computer and are safe
(`git submodule update --init`, the `git pull --rebase` it names on a clean tree,
`git config --global user.name` after asking for the value); hand over the ones they must do
themselves (signing in, installing a plugin with `/plugin`, asking for access). A `note` needs nothing; mention it only if it
explains something they asked about.
4. Run `km doctor` again after fixes and end with what still fails, or that the setup is complete.

## Init (`/km init`)

When the user runs `/km init` (or confirms after "CONVENTIONS.md missing" prompt):
Expand Down
32 changes: 25 additions & 7 deletions skills/km/bin/km
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,29 @@ set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" # -P: follow a symlinked skill install
src="$here/../../../src"
export PYTHONPATH="$src${PYTHONPATH:+:$PYTHONPATH}"
if command -v uv >/dev/null 2>&1; then
exec uv run --quiet --no-project --python '>=3.11' --with pyyaml==6.0.3 python -m km "$@"
# uv, also where its installer puts it before an app restart has put that folder on the PATH
uv="$(command -v uv || true)"
for u in "$HOME/.local/bin/uv" "$HOME/.local/bin/uv.exe"; do [ -n "$uv" ] || { [ -x "$u" ] && uv="$u"; }; done
if [ -n "$uv" ]; then
exec "$uv" run --quiet --no-project --python '>=3.11' --with pyyaml==6.0.3 python -m km "$@"
fi
if ! python3 -c 'import sys, yaml; sys.exit(sys.version_info < (3, 11))' 2>/dev/null; then
echo "km needs uv, or python3 3.11+ with PyYAML" >&2
exit 1
fi
exec python3 -m km "$@"
# Without uv: a Python 3.11+ under any of its usual names (on Windows `python3` is often only the
# Store's placeholder). `km doctor` needs no PyYAML, so it can say what is missing.
cmd=""; skip=""
for a in "$@"; do
if [ -n "$skip" ]; then skip=""; elif [ "$a" = --root ]; then skip=1; elif [ "${a#--root=}" = "$a" ]; then cmd="$a"; break; fi
done
need='import sys; sys.exit(sys.version_info < (3, 11))'
[ "$cmd" = doctor ] || need="import yaml; $need"
for py in python3 python "py -3"; do
if $py -c "$need" >/dev/null 2>&1; then exec $py -m km "$@"; fi
done
# Nothing to run km with: say so in words someone who never used a terminal can act on.
printf '%s\n' >&2 \
"km cannot start on this computer: it needs uv (or Python 3.11+ with PyYAML). Install uv, no" \
"administrator rights needed:" \
' Windows (PowerShell): powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"' \
" macOS and Linux: curl -LsSf https://astral.sh/uv/install.sh | sh" \
"then run the km command again. More: https://docs.astral.sh/uv/getting-started/installation/"
command -v git >/dev/null 2>&1 || echo "git is missing as well: https://git-scm.com/downloads" >&2
exit 1
2 changes: 1 addition & 1 deletion src/km/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""km: knowledge management for markdown knowledge bases."""

__version__ = "1.2.0"
__version__ = "1.3.0"
KM_REF = f"v{__version__}" # this km's release tag; brains pin the commit it points at
3 changes: 2 additions & 1 deletion src/km/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@
"approve": ("km.approve", "sign review docs off as accepted (--by WHO)"),
"init": ("km.init", "create a brain: `init DIR --initials XX`, `--team ...` for a team brain"),
"upgrade": ("km.upgrade", "move a brain's km pins to this version; drop km files copied in earlier"),
"doctor": ("km.doctor", "check this computer is set up for the brain: git, access, plugins, km version"),
}
NO_PIN_CHECK = {"init", "upgrade"} # they write the pins, with the km that is running
NO_PIN_CHECK = {"init", "upgrade", "doctor"} # they write the pins, or check the km that is running
RUNNER = os.environ.get("KM_PINNED_RUNNER", "uvx")


Expand Down
Loading
Loading