@dmthepm/commune
v0.6.0
Published
Maintain a personal wiki from markdown notes, dictated thoughts and everyday work with authoring tools, a shared link graph and portable Astro publishing.
Downloads
375
Maintainers
Readme
Commune
Keep your thinking connected.
Commune helps you maintain a personal wiki from markdown notes and dictated thoughts. Its CLI finds connections while you write. Optional agent skills capture a dump, ask a short round of questions, draft a revision for your review and ship on your word. Its Astro engine publishes linked notes with backlinks and a markdown twin of every page. MIT. It runs devon.md.
- See it. Explore devon.md.
- Start a wiki. Copy the starter below. You need Node 22.12 or newer, npm and Git.
- Report a first run. Tell me where it broke. A failed attempt is useful.
Status on September 8, 2026. Commune runs Devon's personal wiki. A timed install on another machine, an outside author's return edit, and an end to end run with the installed authoring skills are still unproven. The launch checklist lists the evidence still owed.
Start a wiki
The starter is a separate project on the published package. Pick a new folder for it.
git clone https://github.com/dmthepm/commune-wiki.git
cd commune-wiki
node scripts/create-wiki.mjs ../my-wiki
cd ../my-wiki
npm install
npm run build
npm run verify
npm run devOpen the local URL Astro prints. The starter instructions walk through editing a sample note, opening connected notes in panes, and checking backlinks and markdown twins. The copy command installs nothing. npm install runs in your new wiki, and the engine repository itself needs no install.
If you stop or have to guess, tell me where. Include the step, your versions and what happened. No private vault needed. An existing Astro 7 project can skip the starter and use the integration below. The agent skills install separately into a wiki you already have and do not create a site.
Vision and mission
Vision. People can keep their thinking connected, current and in their own hands, and share it in public as it develops.
Mission. Commune turns markdown notes, dictated thoughts and the residue of everyday work into a maintained personal wiki through a shared link graph, authoring tools and portable publishing.
Commune grew out of devon.md. Devon built his own site and recognised the structure afterward, then extracted its tools so someone else could build on them. It is an MIT project with no company behind it and no plans for monetization.
Install
You need an Astro 7 project on Node 22.12 or newer.
pnpm add @dmthepm/commune @astrojs/markdown-remarkThis minimal integration produces plain HTML notes. For the assembled wiki interface, use the starter. Add three files, and your notes. astro.config.mjs, in full:
import { defineConfig } from 'astro/config';
import commune from '@dmthepm/commune/astro';
import { communeMarkdown } from '@dmthepm/commune/markdown';
const site = 'https://example.com';
export default defineConfig({
site,
markdown: { processor: communeMarkdown({ site }) },
integrations: [commune()],
});Astro 7 renders markdown with Sätteri and no longer installs the unified pipeline, so markdown.processor is where the wikilink plugins have to go. site is a parameter because the engine has no host of its own — it decides what counts as an external link against your origin, which you already declare once.
Commune reads markdown off disk, but Astro will not render a page for it until the collection is registered and something routes it — so copy these two files. src/content.config.ts:
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
export const collections = {
notes: defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/notes' }),
schema: z.object({
title: z.string(),
visibility: z.enum(['public', 'private', 'draft']).default('private'),
}),
}),
};And src/pages/notes/[...slug].astro:
---
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const notes = await getCollection('notes', (note) => note.data.visibility === 'public');
return notes.map((note) => ({ params: { slug: note.id }, props: { note } }));
}
const { note } = Astro.props;
const { Content } = await render(note);
---
<html lang="en">
<head><meta charset="utf-8" /><title>{note.data.title}</title></head>
<body><h1>{note.data.title}</h1><Content /></body>
</html>Both are minimal examples. examples/starter provides a separate wiki project. tests/fixtures/consumer tests integration against this repository through a local file dependency; it is a development fixture, not a portable starter.
Notes go in src/content/notes/. Two frontmatter fields are load-bearing:
---
title: "Hello"
visibility: "public"
---
A link to [[World]], and one to [Astro](https://astro.build).title is what [[Hello]] matches on. For notes, visibility defaults to private, so only public is published. Add a second public note with title: "World" before building; the link then resolves and the paragraph renders as:
A link to <a href="/notes/world/" class="wikilink">World</a>, and one to
<a href="https://astro.build" target="_blank" rel="noopener noreferrer">Astro</a>.That route is a starting point, not an interface. The package ships the mechanism and none of src/pages/ — the markup, the layout and the URL shape are yours to change, with Commune resolving links through its shared graph.
What ships
- WikiLinks. Connect notes with
[[Title]], using the target title verbatim and rewriting the sentence around it. The renderer also resolves aliases and[[Title|Display text]], butcheckandgatereport these noncanonical spellings. A link that resolves to nothing stays plain text instead of rendering a dead anchor. - Backlinks. The build writes
backlinks.json— every entry with its inbound and outbound edges — todist/andpublic/.Backlinks.astrorenders it on a page. - Markdown twins. Every published content entry gets its source written beside it, so
/notes/hello/also answers at/notes/hello.md. Entries in the content directories only — a hand-written route undersrc/pages/has no source file to twin. Agents and readers get the same document without scraping HTML. - External links. Anything off your
siteorigin getstarget="_blank" rel="noopener noreferrer"without you marking it up. - The graph as a library.
@dmthepm/commune/graphexports the content loader, the link resolver and the graph builder. The Astro build and the CLI both call it. Sharing the resolver reduces duplicated link logic; it does not guarantee that links never break. - Updates. A fourth collection,
src/content/updates/, for the dated entries that say what changed.Updates.astrorenders the newest few as a card. See Updates below. - Dates with sources.
updated:in frontmatter wins where you wrote one; where you did not, the date comes from the file's last commit, and from its mtime in a tree with no history. Every entry says which, so a page can show where the date came from. See Dates below. - A site-wide last-updated. The build writes
site.jsonbesidebacklinks.json: the newest date across the whole wiki, which entry it belongs to, and the newest commit date whatever the entries claim. - Components and stylesheets.
@dmthepm/commune/components/*.astroand@dmthepm/commune/styles/*.css, shipped as source. These are the components off my own site rather than a theme system — take them as a starting point, not an API.
Dates
Two frontmatter fields decide a page's dates, and neither is required:
---
created: 2025-10-09
updated: 2026-01-21
---Where they are absent the engine reads the repository instead — updated is the file's last commit date, created its first — from one git log walk per build. A tree with no history at all (a tarball, a COPY in a Dockerfile, a host with no git) falls back to file mtimes rather than failing the build.
Every entry carries updatedSource, one of frontmatter, git, mtime or none, and modifiedInGit, which is the commit date whatever updated ended up being. The pair is the point: a note whose updated: says January and whose last commit was September changed on a day nobody wrote down, and a page that shows both says so.
Shallow clones
A shallow checkout produces no derived dates at all. In a --depth 1 clone every file's only commit is the one that was fetched, so every file would date from the day of the build — one confident wrong answer on every entry at once. The engine refuses it rather than reporting it: dates come from frontmatter only, entries without one get updatedSource: "none" and no date, and the build prints this once on stderr:
git history is shallow: dates come from frontmatter only. Fetch full history (fetch-depth: 0 / unshallow) to derive dates from commits.The same refusal applies to file mtimes anywhere inside a repository, and to a file that has never been committed. Inside a checkout an mtime is the moment the file reached that disk — on CI, the moment of the build — so it is the same falsehood wearing a different hat. mtimes are used in one place only: a project that is not in a repository at all.
The fix is to fetch the history. On GitHub Actions, actions/checkout defaults to depth 1, so set it explicitly:
- uses: actions/checkout@v7
with:
fetch-depth: 0On Cloudflare Workers Builds, check the build log for the warning above — if it is there, the clone was shallow. Whether Workers Builds exposes a clone-depth setting is not documented here; if it does not, an alternative is to keep updated: in frontmatter for anything whose date matters, which wins over history anyway. A build step that runs git fetch --unshallow before the build has the same effect wherever the build has network access and credentials for the repository.
Known gap: renames. History is read without --follow, so a file's derived created is the date of the commit that gave it its current path, not the date the writing began. Renaming a note therefore resets its created and leaves updated correct. --follow is per-file by design — it cannot be asked for in the single batched walk this uses — so the fix is a created: in frontmatter, which wins over history.
commune graph query --json carries all four fields. The build writes the site-wide version to site.json:
{
"lastUpdated": "2026-09-02",
"lastUpdatedPath": "/about-this-wiki/",
"lastUpdatedSource": "frontmatter",
"lastModifiedInGit": "2026-09-03",
"entries": 12
}It is a sibling of backlinks.json rather than a key inside it, because every top-level key of backlinks.json is a urlPath and its readers walk it as one. Generated, not committed: a date derived from history changes on the same commit that changes it, so a committed copy would be stale exactly when it mattered. Add public/site.json to your .gitignore.
Updates
A wiki's front door has to answer "what changed" before it answers anything else. Commune's answer is content: one dated entry per batch of work, in src/content/updates/, which the graph treats as a collection like any other — twins, backlinks, check, graph query --collection updates.
---
title: "New notes and a working loop"
date: 2026-09-03
summary: "Rewrote the home note and added two notes."
links:
- Atomic Notes
- /notes/evergreen-notes/
---
I rewrote the home note. [[Atomic Notes]] and [[Evergreen Notes]] are new.links: is the one place in frontmatter where a bare string is a link. Everywhere else a link has to be spelled [[like this]]; a page's own url: would otherwise become a self-edge; but links: means nothing else, so a title or a site path both resolve and both become edges. Write it or don't: [[wikilinks]] in the body work the same way, and naming a page in both places is still one edge.
Register the collection alongside your notes in src/content.config.ts:
updates: defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/updates' }),
schema: z.object({
title: z.string(),
date: z.string(),
summary: z.string(),
aiGenerated: z.boolean().default(false),
links: z.array(z.string()).default([]),
}),
}),Then render the card wherever it belongs — the home page, an index, a sidebar:
---
import Updates from '@dmthepm/commune/components/Updates.astro';
---
<Updates limit={5} heading="Recent updates" />It reads the collection at build time and emits markup. No fetch, no client script.
A feed
The engine ships no routes, so it ships no RSS either — a feed is a route, and routes are yours. It is two lines with @astrojs/rss, in src/pages/updates/rss.xml.ts:
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const updates = await getCollection('updates');
return rss({
title: 'Updates',
description: 'What changed',
site: context.site,
items: updates
.sort((a, b) => b.data.date.localeCompare(a.data.date))
.map((update) => ({
title: update.data.title,
description: update.data.summary,
pubDate: new Date(`${update.data.date}T12:00:00Z`),
link: `/updates/${update.id}/`,
})),
});
}The CLI
Find connections and check your notes while you write, without building the site. The commune executable reads markdown directly from disk and answers without an Astro process running.
commune check
commune graph query --collection notes --orphans
commune graph query --recent 7d
commune update --recent 7d
commune graph related src/content/notes/hello.md
echo "a rough dump that mentions World" | commune graph related -
commune render src/content/notes/hello.md
echo '[[World]]' | commune render -
commune gate| Verb | What it answers |
| --- | --- |
| graph query | Every entry with its edges and dates. Filter with --collection, --tag, --status, --orphans, --deadends, --unreferenced, --recent. |
| graph related <path\|text\|-> | What this connects to. It takes stdin, so you can ask about a draft before it is a note. Titles are matched across whitespace and case, so a dictated "noon tide" still finds Noontide. |
| render <path\|-> | The markdown as HTML, through the site's own pipeline: WikiLinks resolved, external links marked. Takes stdin, so you can see a draft before it is a page. |
| update | Scaffold a dated update entry from what changed. Prints it; --write files it. |
| check | Broken links, duplicate names, ambiguous targets, non-canonical titles. |
| gate | Run after a build, against the built site. |
--orphans and --unreferenced are different questions and it is worth knowing which one you are asking. An orphan is isolated — nothing links to it and it links nowhere — which on a wiki where most notes link out returns almost nothing. --unreferenced is zero inbound with any outbound: the note you wrote, cited three others from, and never linked back to from anywhere. updates entries are left out of it, since a dated changelog entry is expected to have nothing pointing at it; --collection updates is how you say you meant them.
--recent takes 7d, 2w or a date, and reports the day it resolved to in the summary — which is what a weekly update job needs, since 7d means a different day tomorrow. Entries with no date at all are not returned: "unchanged since Monday" and "nobody knows" are different answers.
Every verb takes --json and emits one document on stdout with everything else on stderr. The human-readable text is the fallback rendering; the JSON is the contract.
render is the site's own markdown processor with no Astro process around it — the same communeMarkdown() the config hands to markdown.processor, so the HTML is the page's HTML rather than a lookalike. It needs the site's origin to decide which links are external: --site says it, and without one the Astro config is read for a site declaration, falling back to https://example.com with a line on stderr saying so. --json adds the document's links and the names among them that resolve to nothing, which the HTML cannot tell you — an unresolved WikiLink renders as plain text, exactly as it does on the site.
update is the only verb that can write, and it only does so when asked: without --write the entry goes to stdout, and with it the command refuses to overwrite an update that already exists. summary comes out empty — summarizing a week is a judgement, and the CLI has none.
Exit codes report whether the command finished, never what it found — 0 finished, 1 could not finish, 2 invalid invocation. Findings live in the payload. A command that exits non-zero because it found something is indistinguishable, to a shell, from one that crashed. gate is the one deliberate exception: a gate's entire job is a yes/no and a build has to stop on it, so gate exits 1 when the build it checked is wrong.
commune --help prints the full surface. commune --version prints the installed version, which is the way to know what you have.
Author with the skills
These optional skills require an existing Commune wiki and access to Claude Code or Codex. They do not install the engine or scaffold a site. Install the four authoring skills from this repository.
npx skills add dmthepm/commune-wikiThe skills install for Claude Code and Codex, globally or into one project. They call the wiki's existing node_modules/.bin/commune directly, so authoring and publishing use the same CLI. They require version 0.4.0 or newer, check it first and install nothing themselves.
commune-setup runs once per wiki and writes its WRITING.md rules. commune-dump saves dictated or pasted text verbatim to dumps/<slug>.md and records connection candidates and the check baseline in dumps/<slug>.connect.md. Here <slug> includes the capture date.
commune-write asks one short round of editorial questions and waits for answers in dumps/<slug>.answers.md. It then drafts into the note and renders the original and draft side by side in dumps/<slug>.review.html for the author to review.
On the author's instruction, commune-ship compares finding identities against the baseline, files an update, builds, gates and verifies each new href and destination file. It commits according to WRITING.md's dumps.commit policy, opens a PR and records the receipt in dumps/<slug>.ship.md. The author approves the content. This skill never merges. That boundary governs authored content, while code maintenance follows the repository's contribution rules.
The four skills, their tests and the WRITING.md template ship today. The loop was run once end to end by hand before the skills existed. An end-to-end run using the installed skills remains unverified.
Files and publishing
Commune is not a note-taking app. It works with markdown files on disk, written in whatever editor you like, and provides no editor, sync service, account or server of its own. The files remain yours whether or not you ever run it.
Only notes with visibility: public enter the graph and publishing output. Research, pages and updates are included regardless of visibility. Handoffs in dumps/ are committed by default under the writing policy. They are outside the engine's content collections and do not automatically become site pages.
What needs testing
The launch checklist tracks first-install, installed-skills and return-edit proof, plus browsing and editing in Obsidian. Features follow what those sessions show is needed. Work is tracked in the issues.
Deploy
The build output is dist/, a static directory with no runtime, so any static host serves it — see docs/hosting.md.
Working on Commune itself
Use Node 22.18+ (the current Node 22 release selected by .nvmrc) and pnpm 10. The repository runs TypeScript source directly in its tests. The published package has the lower Node 22.12+ minimum.
pnpm install
pnpm dev # the engine's own wiki, for developing against
pnpm build # compile lib/, build the site, then gate it
pnpm test # node --testpnpm test:consumer installs tests/fixtures/consumer against the working tree and builds it. This checks the package boundary locally. It does not substitute for an independent first install.
Run pnpm test:starter from the repository root to check the copier, destination safeguards and published dependency declarations. The registry install test is skipped by default. COMMUNE_STARTER_INSTALL=1 pnpm test:starter also copies the starter into a temporary directory, installs from npm, builds and verifies its output. Neither mode proves a timed first install on another machine.
CONTRIBUTING.md has the rest. Issues and questions go to the tracker.
License
MIT — see LICENSE. Use it, change it, sell it. Keep the copyright notice; that is the whole obligation.
