@dmthepm/commune 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ Start a wiki in one paste. You need Node 22.12 or newer, npm and Git.
12
12
  git clone --depth 1 https://github.com/dmthepm/commune-wiki.git && node commune-wiki/scripts/create-wiki.mjs my-wiki && cd my-wiki && npm install && npm run dev
13
13
  ```
14
14
 
15
- Open the address Astro prints. The clone is only the source of the copy, and you can delete it afterward. If you stop or have to guess, [tell me where it broke](https://github.com/dmthepm/commune-wiki/issues/85). A failed attempt is useful.
15
+ Open the address Astro prints. The clone is only the source of the copy, and you can delete it afterward. To run the paste again, delete `commune-wiki` and `my-wiki` first, since both commands refuse to overwrite a folder. Run by a coding agent on macOS or Linux, Astro 7 starts the dev server in the background and prints its address as JSON. `npx astro dev stop` stops it. If you stop or have to guess, [tell me where it broke](https://github.com/dmthepm/commune-wiki/issues/85). A failed attempt is useful.
16
16
 
17
17
  ## Start a wiki
18
18
 
@@ -265,16 +265,18 @@ export async function GET(context) {
265
265
 
266
266
  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.
267
267
 
268
+ In a wiki, the executable lives in `node_modules/.bin`, so run it through `npx`. The paths below are the starter's sample notes.
269
+
268
270
  ```bash
269
- commune check
270
- commune graph query --collection notes --orphans
271
- commune graph query --recent 7d
272
- commune update --recent 7d
273
- commune graph related src/content/notes/hello.md
274
- echo "a rough dump that mentions World" | commune graph related -
275
- commune render src/content/notes/hello.md
276
- echo '[[World]]' | commune render -
277
- commune gate
271
+ npx commune check
272
+ npx commune graph query --collection notes --orphans
273
+ npx commune graph query --recent 7d
274
+ npx commune update --recent 7d
275
+ npx commune graph related src/content/notes/welcome.md
276
+ echo "a rough dump that mentions Connected notes" | npx commune graph related -
277
+ npx commune render src/content/notes/welcome.md
278
+ echo '[[Connected notes]]' | npx commune render -
279
+ npx commune gate
278
280
  ```
279
281
 
280
282
  | Verb | What it answers |
@@ -305,16 +307,16 @@ Exit codes report whether the command finished, never what it found — `0` fini
305
307
  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.
306
308
 
307
309
  ```bash
308
- npx skills add dmthepm/commune-wiki
310
+ npx skills add dmthepm/commune-wiki -a claude-code codex -s '*' -y
309
311
  ```
310
312
 
311
- The 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.
313
+ That installs all four skills into the current project for Claude Code and Codex without prompts. Without the flags the installer asks, starts with no skills selected and lists Claude Code unticked among dozens of agents. Add `-g` to install for every project instead. 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.
312
314
 
313
315
  `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.
314
316
 
315
317
  `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.
316
318
 
317
- 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.
319
+ 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`. That needs the wiki to be a Git repository with a GitHub remote and a way to open a pull request, such as `gh` signed in. The starter copy is neither, so run `git init` in it and push it to a repository of your own first. The author approves the content. This skill never merges. That boundary governs authored content, while code maintenance follows the repository's contribution rules.
318
320
 
319
321
  The four skills, their tests and the `WRITING.md` template ship today. An end to end run of the installed skills on a fresh starter wiki, from dictation to a gated commit, passed on October 1, 2026. Pushing and opening the pull request were not part of that run.
320
322
 
package/lib/cli/check.js CHANGED
@@ -10,9 +10,9 @@
10
10
  * v1 is scoped to link integrity so it does not block on #17's collection
11
11
  * collapse. Frontmatter drift is a follow-up.
12
12
  */
13
- import { buildGraph, checkEntries, loadContentEntries, } from "../lib/graph.js";
14
- import { SCHEMA, writeJson, writeLines } from "./output.js";
15
- import { EXIT_OK } from "./errors.js";
13
+ import { buildGraph, checkEntries, loadContentEntries, } from '../lib/graph.js';
14
+ import { SCHEMA, writeJson, writeLines } from './output.js';
15
+ import { EXIT_OK } from './errors.js';
16
16
  const RULES = [
17
17
  'broken-link',
18
18
  'ambiguous-target',
package/lib/cli/gate.js CHANGED
@@ -28,9 +28,9 @@
28
28
  */
29
29
  import { readFile } from 'node:fs/promises';
30
30
  import path from 'node:path';
31
- import { findNoncanonicalTitles, loadContentEntries, stripCode, } from "../lib/graph.js";
32
- import { EXIT_FAILED, EXIT_OK, failure } from "./errors.js";
33
- import { SCHEMA, writeJson } from "./output.js";
31
+ import { findNoncanonicalTitles, loadContentEntries, stripCode, } from '../lib/graph.js';
32
+ import { EXIT_FAILED, EXIT_OK, failure } from './errors.js';
33
+ import { SCHEMA, writeJson } from './output.js';
34
34
  const WIKILINK = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
35
35
  async function readSearchIndex(root) {
36
36
  // The public artifact, not the built one: `public/backlinks.json` is what
@@ -148,7 +148,14 @@ export async function gateCommand(root, dist, json) {
148
148
  ...(await pageLinksRender(entries, pages, distDir)),
149
149
  ];
150
150
  const passed = failures.length === 0;
151
- const summary = `${pages.length} standalone page${pages.length === 1 ? '' : 's'} indexed for search and linked from notes`;
151
+ // Name what was checked, not only the standalone-page count. A starter
152
+ // wiki has no standalone pages, and "0 pages indexed" alone reads as if
153
+ // the gate looked at nothing.
154
+ const checked = `${entries.length} ${entries.length === 1 ? 'entry' : 'entries'} checked, every resolved WikiLink uses its target's exact title`;
155
+ const indexed = pages.length
156
+ ? `${pages.length} standalone page${pages.length === 1 ? '' : 's'} indexed for search and linked from notes`
157
+ : 'no standalone pages to index';
158
+ const summary = `${checked}, ${indexed}`;
152
159
  if (json) {
153
160
  writeJson({ schema: SCHEMA, passed, failures });
154
161
  }
package/lib/cli/main.js CHANGED
@@ -12,17 +12,17 @@
12
12
  * JSON: `--json` is known before the strict parse that rejects the argv.
13
13
  */
14
14
  import { parseArgs } from 'node:util';
15
- import { CliError, EXIT_OK, EXIT_USAGE, isParseArgsError, usageError } from "./errors.js";
16
- import { writeError } from "./output.js";
17
- import { resolveRoot } from "./root.js";
18
- import { toIsoDay } from "../lib/graph.js";
19
- import { parseRecent, queryCommand } from "./query.js";
20
- import { checkCommand } from "./check.js";
21
- import { gateCommand } from "./gate.js";
22
- import { relatedCommand } from "./related.js";
23
- import { updateCommand } from "./update.js";
24
- import { COMMAND_USAGE, USAGE } from "./usage.js";
25
- import { readVersion } from "./version.js";
15
+ import { CliError, EXIT_OK, EXIT_USAGE, isParseArgsError, usageError } from './errors.js';
16
+ import { writeError } from './output.js';
17
+ import { resolveRoot } from './root.js';
18
+ import { toIsoDay } from '../lib/graph.js';
19
+ import { parseRecent, queryCommand } from './query.js';
20
+ import { checkCommand } from './check.js';
21
+ import { gateCommand } from './gate.js';
22
+ import { relatedCommand } from './related.js';
23
+ import { updateCommand } from './update.js';
24
+ import { COMMAND_USAGE, USAGE } from './usage.js';
25
+ import { readVersion } from './version.js';
26
26
  /** Options understood everywhere, in any position. */
27
27
  const GLOBAL = {
28
28
  root: { type: 'string' },
@@ -173,7 +173,7 @@ async function dispatch(args) {
173
173
  // `check`, and an outright failure in a project that has the CLI but
174
174
  // not the renderer, which is the opposite of the promise the rest of
175
175
  // this CLI makes about running without Astro.
176
- const { renderCommand } = await import("./render.js");
176
+ const { renderCommand } = await import('./render.js');
177
177
  return renderCommand(await resolveRoot(values.root), positionals[0], site, values.json);
178
178
  }
179
179
  case 'check': {
package/lib/cli/query.js CHANGED
@@ -6,9 +6,9 @@
6
6
  * per note. Filters narrow which entries are *returned*; they never change how
7
7
  * links resolve, so an entry's degree is the same whether or not you filtered.
8
8
  */
9
- import { buildGraph, loadContentEntries, toIsoDay, } from "../lib/graph.js";
10
- import { SCHEMA, writeJson, writeLines } from "./output.js";
11
- import { EXIT_OK } from "./errors.js";
9
+ import { buildGraph, loadContentEntries, toIsoDay, } from '../lib/graph.js';
10
+ import { SCHEMA, writeJson, writeLines } from './output.js';
11
+ import { EXIT_OK } from './errors.js';
12
12
  /**
13
13
  * Resolve `--recent` to the day it means.
14
14
  *
@@ -17,10 +17,10 @@
17
17
  * written `Noontide` are the same name spelled two ways, and matching them is
18
18
  * still an exact match — just of the right string.
19
19
  */
20
- import { buildGraph, buildLinkLookup, buildUrlLookup, extractLinks, loadContentEntries, resolveLink, stripCode, } from "../lib/graph.js";
21
- import { SCHEMA, writeJson, writeLines } from "./output.js";
22
- import { EXIT_OK } from "./errors.js";
23
- import { readSource } from "./source.js";
20
+ import { buildGraph, buildLinkLookup, buildUrlLookup, extractLinks, loadContentEntries, resolveLink, stripCode, } from '../lib/graph.js';
21
+ import { SCHEMA, writeJson, writeLines } from './output.js';
22
+ import { EXIT_OK } from './errors.js';
23
+ import { readSource } from './source.js';
24
24
  /**
25
25
  * Shortest name worth matching, measured after normalisation. Below this, a
26
26
  * mention is noise — `B` would make every capital B a connection.
package/lib/cli/render.js CHANGED
@@ -19,12 +19,12 @@
19
19
  * the time there is HTML the broken link is indistinguishable from a sentence,
20
20
  * and the one thing a reviewer most needs to be told is gone.
21
21
  */
22
- import { communeMarkdown } from "../markdown.js";
23
- import { buildLinkLookup, buildUrlLookup, extractLinks, loadContentEntries, resolveLink, } from "../lib/graph.js";
24
- import { SCHEMA, writeJson } from "./output.js";
25
- import { EXIT_OK } from "./errors.js";
26
- import { readSource } from "./source.js";
27
- import { resolveSite } from "./site.js";
22
+ import { communeMarkdown } from '../markdown.js';
23
+ import { buildLinkLookup, buildUrlLookup, extractLinks, loadContentEntries, resolveLink, } from '../lib/graph.js';
24
+ import { SCHEMA, writeJson } from './output.js';
25
+ import { EXIT_OK } from './errors.js';
26
+ import { readSource } from './source.js';
27
+ import { resolveSite } from './site.js';
28
28
  export async function renderCommand(root, input, siteFlag, json) {
29
29
  // Read before anything else: `-` has to consume stdin before a slow scan of
30
30
  // the content tree, or a producer writing into the pipe waits on us.
package/lib/cli/root.js CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { stat } from 'node:fs/promises';
12
12
  import path from 'node:path';
13
- import { failure } from "./errors.js";
13
+ import { failure } from './errors.js';
14
14
  export async function resolveRoot(value) {
15
15
  const root = path.resolve(value ?? process.cwd());
16
16
  const content = path.join(root, 'src', 'content');
package/lib/cli/source.js CHANGED
@@ -15,7 +15,7 @@
15
15
  import { readFile, stat } from 'node:fs/promises';
16
16
  import path from 'node:path';
17
17
  import matter from 'gray-matter';
18
- import { failure } from "./errors.js";
18
+ import { failure } from './errors.js';
19
19
  async function isFile(candidate) {
20
20
  try {
21
21
  return (await stat(candidate)).isFile();
package/lib/cli/update.js CHANGED
@@ -15,9 +15,9 @@
15
15
  */
16
16
  import { mkdir, stat, writeFile } from 'node:fs/promises';
17
17
  import path from 'node:path';
18
- import { CONTENT_DIRS, loadContentEntries } from "../lib/graph.js";
19
- import { SCHEMA, writeJson, writeLines } from "./output.js";
20
- import { EXIT_OK, failure } from "./errors.js";
18
+ import { CONTENT_DIRS, loadContentEntries } from '../lib/graph.js';
19
+ import { SCHEMA, writeJson, writeLines } from './output.js';
20
+ import { EXIT_OK, failure } from './errors.js';
21
21
  /**
22
22
  * The entry, as markdown.
23
23
  *
@@ -8,10 +8,10 @@
8
8
  * core in `src/lib/graph.ts`, so the `commune` CLI produces the same graph
9
9
  * without an Astro process anywhere in sight.
10
10
  */
11
- import { copyFile, writeFile, mkdir } from 'node:fs/promises';
11
+ import { copyFile, readFile, writeFile, mkdir } from 'node:fs/promises';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import path from 'node:path';
14
- import { buildGraph, formatDiagnostic, loadContentEntries, summarizeSite, toBacklinksJson, toMarkdownPath, } from "./lib/graph.js";
14
+ import { buildGraph, formatDiagnostic, loadContentEntries, summarizeSite, toBacklinksJson, toMarkdownPath, } from './lib/graph.js';
15
15
  function buildBacklinksGraph(entries, logger) {
16
16
  const graph = buildGraph(entries);
17
17
  logger.info(`📝 Found ${Object.keys(graph.nodes).length} public content entries`);
@@ -64,6 +64,37 @@ async function writeMarkdownFiles(entries, root, outDir) {
64
64
  }
65
65
  return entries.length;
66
66
  }
67
+ /**
68
+ * The dev server's answer to a `<url>.md` request: the same source file the
69
+ * build copies, read on each request so an edit shows up without a restart.
70
+ *
71
+ * The build writes twins in `astro:build:done`, which `astro dev` never runs,
72
+ * so without this a page's "view as markdown" link 404s in exactly the server
73
+ * a first run lands in. The lookup goes through `toMarkdownPath`, the mapping
74
+ * the build writer uses, so dev and build cannot disagree about which URL is
75
+ * which file. Anything that is not a twin falls through to Astro.
76
+ */
77
+ async function findMarkdownTwin(root, pathname) {
78
+ if (!pathname.endsWith('.md'))
79
+ return undefined;
80
+ let requested;
81
+ try {
82
+ requested = decodeURI(pathname).replace(/^\/+/, '');
83
+ }
84
+ catch {
85
+ return undefined;
86
+ }
87
+ const entries = await loadContentEntries({ root });
88
+ const entry = entries.find((candidate) => {
89
+ try {
90
+ return toMarkdownPath(candidate.urlPath) === requested;
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ });
96
+ return entry ? path.join(root, entry.file) : undefined;
97
+ }
67
98
  function summarize(graph) {
68
99
  return `📊 ${graph.totalBacklinks} total backlinks across ${Object.keys(graph.nodes).length} entries`;
69
100
  }
@@ -105,6 +136,32 @@ export default function commune(_options = {}) {
105
136
  throw error;
106
137
  }
107
138
  },
139
+ 'astro:server:setup': ({ server, logger }) => {
140
+ server.middlewares.use((request, response, next) => {
141
+ const pathname = new URL(request.url ?? '/', 'http://localhost').pathname;
142
+ if (request.method !== 'GET' && request.method !== 'HEAD')
143
+ return next();
144
+ if (!pathname.endsWith('.md'))
145
+ return next();
146
+ // Vite's own module and file requests are never twins, and each
147
+ // lookup reads the whole vault, so they skip it.
148
+ if (/^\/(?:src\/|node_modules\/|@)/.test(pathname))
149
+ return next();
150
+ findMarkdownTwin(root, pathname)
151
+ .then(async (file) => {
152
+ if (!file)
153
+ return next();
154
+ const source = await readFile(file);
155
+ response.setHeader('Content-Type', 'text/markdown; charset=utf-8');
156
+ response.setHeader('Content-Length', source.byteLength);
157
+ response.end(request.method === 'HEAD' ? undefined : source);
158
+ })
159
+ .catch((error) => {
160
+ logger.error(`Failed to serve ${pathname}: ${String(error)}`);
161
+ next(error);
162
+ });
163
+ });
164
+ },
108
165
  'astro:build:done': async ({ dir, logger }) => {
109
166
  logger.info('🔗 Building backlinks index...');
110
167
  try {
package/lib/lib/graph.js CHANGED
@@ -16,13 +16,13 @@
16
16
  * - the title/alias lookup used to resolve `[[WikiLinks]]`
17
17
  * - which link forms count as an edge, and how each one resolves
18
18
  */
19
- import { globby } from 'globby';
19
+ import { glob } from 'tinyglobby';
20
20
  import { slug as githubSlug } from 'github-slugger';
21
21
  import matter from 'gray-matter';
22
22
  import { readFile } from 'node:fs/promises';
23
23
  import path from 'node:path';
24
- import { readContentHistory, readMtimeDate, } from "./dates.js";
25
- export { SHALLOW_WARNING, toIsoDay } from "./dates.js";
24
+ import { readContentHistory, readMtimeDate, } from './dates.js';
25
+ export { SHALLOW_WARNING, toIsoDay } from './dates.js';
26
26
  /** Where each collection's markdown lives, relative to the project root. */
27
27
  export const CONTENT_DIRS = {
28
28
  notes: 'src/content/notes',
@@ -206,9 +206,12 @@ export async function loadContentEntries(options = {}) {
206
206
  // alternative is a `git log` per file, which is a child process per note.
207
207
  const history = await readContentHistory(root, Object.values(CONTENT_DIRS));
208
208
  for (const collection of COLLECTIONS) {
209
- // `cwd` keeps globby's results root-relative, which is exactly the
209
+ // `cwd` keeps tinyglobby's results root-relative, which is exactly the
210
210
  // spelling `ContentEntry.file` promises; only the read needs the join.
211
- const files = await globby(`${CONTENT_DIRS[collection]}/**/*.{md,mdx}`, { cwd: root });
211
+ const files = await glob(`${CONTENT_DIRS[collection]}/**/*.{md,mdx}`, {
212
+ cwd: root,
213
+ expandDirectories: false,
214
+ });
212
215
  files.sort();
213
216
  for (const file of files) {
214
217
  const source = await readFile(path.join(root, file), 'utf8');
package/lib/markdown.js CHANGED
@@ -14,8 +14,8 @@
14
14
  * own origin, which lives once in their `defineConfig({ site })`.
15
15
  */
16
16
  import { unified } from '@astrojs/markdown-remark';
17
- import remarkWikiLinks from "./remark-wikilinks.js";
18
- import rehypeExternalLinks from "./rehype-external-links.js";
17
+ import remarkWikiLinks from './remark-wikilinks.js';
18
+ import rehypeExternalLinks from './rehype-external-links.js';
19
19
  export function communeMarkdown({ site, root } = {}) {
20
20
  return unified({
21
21
  remarkPlugins: [[remarkWikiLinks, { root }]],
@@ -10,7 +10,7 @@
10
10
  * graph core in `src/lib/graph.ts` — not here.
11
11
  */
12
12
  import { visit } from 'unist-util-visit';
13
- import { getLinkLookup, linkKey } from "./lib/graph.js";
13
+ import { getLinkLookup, linkKey } from './lib/graph.js';
14
14
  /**
15
15
  * Remark plugin function.
16
16
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dmthepm/commune",
3
3
  "type": "module",
4
- "version": "0.6.3",
4
+ "version": "0.6.5",
5
5
  "description": "Maintain a personal wiki from markdown notes, dictated thoughts and everyday work with authoring tools, a shared link graph and portable Astro publishing.",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -82,8 +82,8 @@
82
82
  },
83
83
  "dependencies": {
84
84
  "github-slugger": "^2.0.0",
85
- "globby": "^16.2.4",
86
85
  "gray-matter": "^4.0.3",
86
+ "tinyglobby": "^0.2.17",
87
87
  "unist-util-visit": "^5.1.0"
88
88
  },
89
89
  "devDependencies": {
@@ -94,6 +94,6 @@
94
94
  "@types/node": "^22.20.1",
95
95
  "astro": "^7.2.10",
96
96
  "tailwindcss": "^3.4.0",
97
- "typescript": "^5.6.0"
97
+ "typescript": "^7.0.2"
98
98
  }
99
99
  }