@kontextmind/kxm 0.7.188 → 0.7.189
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/.claude-plugin/marketplace.json +1 -1
- package/docs/contributing/ci-and-release.md +1 -1
- package/docs/contributing/development.md +2 -2
- package/docs/contributing/writing-docs.md +9 -2
- package/package.json +2 -2
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/scripts/check-plan-links.d.mts +4 -0
- package/scripts/check-plan-links.mjs +234 -0
|
@@ -39,7 +39,7 @@ together.
|
|
|
39
39
|
|---|---|---|
|
|
40
40
|
| Before you push | Local | `npm run verify` |
|
|
41
41
|
| Pull request, push to `main`, or manual | `ci.yml` (`required`) | Aggregates the lanes below into one pass/fail check named `CI / required` |
|
|
42
|
-
| Pull request and push | `ci.yml` (Docs lint) | `lint:docs` and `check:versions`, always |
|
|
42
|
+
| Pull request and push | `ci.yml` (Docs lint) | `lint:docs` (markdownlint and the `plans/` relative-link check) and `check:versions`, always |
|
|
43
43
|
| Code pull request and code push | `ci.yml` (Unit engine and Unit, Linux Node 24) | `engine.test.ts` split by test name; `permission.test.ts`, `runtime.test.ts`, and `package-install.test.ts` one file at a time; every other unit file in two light shards. Typecheck and `check-generated` run on `light-1` |
|
|
44
44
|
| Code pull request and code push | `ci.yml` (Python control plane) | Python 3.13 `unittest` contracts from the frozen uv workspace against PostgreSQL 17 started by `scripts/ci-postgres-17.sh` |
|
|
45
45
|
| Platform-sensitive pull request | `ci.yml` (Unit, Windows Node 24) | The same unit lanes on `windows-latest` |
|
|
@@ -255,7 +255,7 @@ It runs three scripts in order:
|
|
|
255
255
|
| Step | Script | What it checks |
|
|
256
256
|
|---|---|---|
|
|
257
257
|
| 1 | `npm test` | Build, then `test/core/*.test.ts` and `packages/core/*/tests/unit/*.test.ts`. The script passes `--test-force-exit` so a finished run leaves the process even if a handle is still open; a failing test is still reported before that exit |
|
|
258
|
-
| 2 | `npm run check` | `tsc --noEmit`, `
|
|
258
|
+
| 2 | `npm run check` | `tsc --noEmit`, markdownlint, the `plans/` relative-link check, and version surfaces |
|
|
259
259
|
| 3 | `npm run check:generated` | Generated artifacts are tracked and match the staged copy |
|
|
260
260
|
|
|
261
261
|
Changes to the Python control plane also run its frozen-workspace contract
|
|
@@ -303,7 +303,7 @@ Other scripts you will use:
|
|
|
303
303
|
| `npm run test:simulations` | `test/simulations/*.test.ts` |
|
|
304
304
|
| `npm run test:coverage` | Core suite with coverage floors |
|
|
305
305
|
| `npm run test:coverage:complete` | Core plus simulations with the higher nightly floors |
|
|
306
|
-
| `npm run lint:docs` | Markdown lint
|
|
306
|
+
| `npm run lint:docs` | Markdown lint, then the `plans/` relative-link and heading check |
|
|
307
307
|
| `npm run e2e` | Start Obscura if needed, then Playwright in `test/e2e/` (`node --test` does not collect that directory) |
|
|
308
308
|
| `npm run validate:pr` | The three-minute CI gate; see [CI and release](ci-and-release.md) |
|
|
309
309
|
| `npm run validate:ci` | Coverage suite, `check` and a package dry run (not run by CI today) |
|
|
@@ -297,8 +297,15 @@ exist only in prose, never in command form.
|
|
|
297
297
|
|
|
298
298
|
### Check links
|
|
299
299
|
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
`npm run lint:docs` runs markdownlint, then `scripts/check-plan-links.mjs`.
|
|
301
|
+
That script checks every Markdown link under `plans/`. A relative target has
|
|
302
|
+
to exist. A `#heading` on a Markdown file has to match a heading in that
|
|
303
|
+
file. `related` entries in plan front matter are checked the same way, relative
|
|
304
|
+
to the file. Docs lint runs this on every pull request.
|
|
305
|
+
|
|
306
|
+
For a page outside `plans/`, run this check on the files you changed. It
|
|
307
|
+
prints each relative link whose target is missing, and it does not check
|
|
308
|
+
anchors:
|
|
302
309
|
|
|
303
310
|
```bash
|
|
304
311
|
node --input-type=module -e '
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontextmind/kxm",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.189",
|
|
4
4
|
"description": "KXM local-first multi-agent orchestration and operator dashboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "KontextMind",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"test:coverage:complete": "npm run build && node scripts/run-bounded.mjs 2400000 node --disable-warning=ExperimentalWarning --experimental-strip-types --test --test-force-exit --test-concurrency=4 --experimental-test-coverage --test-coverage-lines=93 --test-coverage-branches=80 --test-coverage-functions=93 --test-coverage-include=plugins/kxm/src/**/*.ts --test-coverage-exclude=plugins/kxm/src/server.ts --test-coverage-exclude=plugins/kxm/src/mcp-server.ts --test-coverage-exclude=plugins/kxm/src/runtime-supervisor.ts --test-coverage-include=packages/core/*/src/**/*.ts test/core/*.test.ts test/simulations/*.test.ts packages/core/*/tests/unit/*.test.ts",
|
|
45
45
|
"test:coverage": "npm run test:coverage:core",
|
|
46
46
|
"typecheck": "tsc --noEmit",
|
|
47
|
-
"lint:docs": "markdownlint-cli2",
|
|
47
|
+
"lint:docs": "markdownlint-cli2 && node scripts/check-plan-links.mjs",
|
|
48
48
|
"check:versions": "node scripts/check-versions.mjs",
|
|
49
49
|
"check": "npm run typecheck && npm run lint:docs && npm run check:versions && node scripts/workforce-lint.mjs",
|
|
50
50
|
"verify": "npm test && npm run check && npm run check:generated",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "kxm",
|
|
4
4
|
"displayName": "KXM",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.189",
|
|
6
6
|
"description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "KontextMind",
|
|
@@ -25462,7 +25462,7 @@ function sessionTokenFixHint(policy) {
|
|
|
25462
25462
|
}
|
|
25463
25463
|
|
|
25464
25464
|
// plugins/kxm/src/mcp-server.ts
|
|
25465
|
-
var VERSION = "0.7.
|
|
25465
|
+
var VERSION = "0.7.189";
|
|
25466
25466
|
var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
25467
25467
|
var inbox = /* @__PURE__ */ new Map();
|
|
25468
25468
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
package/plugins/kxm/package.json
CHANGED
|
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
|
|
|
11
11
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
12
12
|
import { sessionTokenFixHint } from "./session-token-hint.ts";
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.7.
|
|
14
|
+
const VERSION = "0.7.189";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Relative links and heading anchors under plans/.
|
|
4
|
+
// Docs lint runs this. It does not call the network.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
|
|
12
|
+
export function githubSlug(value) {
|
|
13
|
+
return value.toLowerCase().replace(/[^\p{L}\p{M}\p{N}\p{Pc}\- ]/gu, "").replace(/ /g, "-");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function headingText(raw) {
|
|
17
|
+
let text = raw.trim().replace(/\s+#+\s*$/, "");
|
|
18
|
+
text = text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
19
|
+
text = text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
20
|
+
text = text.replace(/<([^>\s]+)>/g, "$1");
|
|
21
|
+
text = text.replace(/`([^`]*)`/g, "$1");
|
|
22
|
+
text = text.replace(/<\/?[^>]+>/g, "");
|
|
23
|
+
text = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
24
|
+
text = text.replace(/"/g, '"').replace(/'/g, "'");
|
|
25
|
+
return text;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function headingIds(markdown) {
|
|
29
|
+
const lines = markdown.split("\n");
|
|
30
|
+
const ids = new Set();
|
|
31
|
+
const occurrences = Object.create(null);
|
|
32
|
+
const own = Object.hasOwnProperty;
|
|
33
|
+
let index = 0;
|
|
34
|
+
let fence = null;
|
|
35
|
+
if (lines[0]?.trim() === "---") {
|
|
36
|
+
index = 1;
|
|
37
|
+
while (index < lines.length && lines[index].trim() !== "---") index += 1;
|
|
38
|
+
index += 1;
|
|
39
|
+
}
|
|
40
|
+
function addSlug(text) {
|
|
41
|
+
const original = githubSlug(headingText(text));
|
|
42
|
+
let result = original;
|
|
43
|
+
while (own.call(occurrences, result)) {
|
|
44
|
+
occurrences[original] = (occurrences[original] || 0) + 1;
|
|
45
|
+
result = `${original}-${occurrences[original]}`;
|
|
46
|
+
}
|
|
47
|
+
occurrences[result] = 0;
|
|
48
|
+
ids.add(result);
|
|
49
|
+
}
|
|
50
|
+
for (; index < lines.length; index += 1) {
|
|
51
|
+
const line = lines[index];
|
|
52
|
+
const opened = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
53
|
+
if (opened && !fence) {
|
|
54
|
+
fence = opened[1];
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (fence && new RegExp(`^ {0,3}${fence[0]}{${fence.length},}\\s*$`).test(line)) {
|
|
58
|
+
fence = null;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (fence) continue;
|
|
62
|
+
const atx = line.match(/^(#{1,6})\s+(.+?)\s*$/);
|
|
63
|
+
if (atx) addSlug(atx[2]);
|
|
64
|
+
for (const match of line.matchAll(/\sid=["']([^"']+)["']/g)) ids.add(match[1]);
|
|
65
|
+
for (const match of line.matchAll(/\sname=["']([^"']+)["']/g)) ids.add(match[1]);
|
|
66
|
+
}
|
|
67
|
+
return ids;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function visibleLines(markdown) {
|
|
71
|
+
const lines = markdown.split("\n");
|
|
72
|
+
const visible = [];
|
|
73
|
+
let fence = null;
|
|
74
|
+
let comment = false;
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
if (!fence && !comment) {
|
|
77
|
+
const opened = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
78
|
+
if (opened) {
|
|
79
|
+
fence = opened[1];
|
|
80
|
+
visible.push("");
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (fence) {
|
|
85
|
+
if (new RegExp(`^ {0,3}${fence[0]}{${fence.length},}\\s*$`).test(line)) fence = null;
|
|
86
|
+
visible.push("");
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
let rest = line;
|
|
90
|
+
let kept = "";
|
|
91
|
+
while (rest.length > 0) {
|
|
92
|
+
if (comment) {
|
|
93
|
+
const end = rest.indexOf("-->");
|
|
94
|
+
if (end < 0) {
|
|
95
|
+
rest = "";
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
comment = false;
|
|
99
|
+
rest = rest.slice(end + 3);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const start = rest.indexOf("<!--");
|
|
103
|
+
if (start < 0) {
|
|
104
|
+
kept += rest;
|
|
105
|
+
rest = "";
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
kept += rest.slice(0, start);
|
|
109
|
+
rest = rest.slice(start + 4);
|
|
110
|
+
comment = true;
|
|
111
|
+
}
|
|
112
|
+
visible.push(kept);
|
|
113
|
+
}
|
|
114
|
+
return visible;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function destinationOf(raw) {
|
|
118
|
+
let dest = raw.trim();
|
|
119
|
+
if (dest.startsWith("<") && dest.endsWith(">")) dest = dest.slice(1, -1).trim();
|
|
120
|
+
dest = dest.replace(/\s+"[^"]*"$/, "").replace(/\s+'[^']*'$/, "");
|
|
121
|
+
return dest;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function problemsForDestination(file, lineNumber, dest) {
|
|
125
|
+
if (!dest || /^[a-z][a-z0-9+.-]*:/i.test(dest)) return [];
|
|
126
|
+
if (dest.startsWith("//")) return [];
|
|
127
|
+
if (dest.startsWith("/")) {
|
|
128
|
+
return [`${file}:${lineNumber}: absolute path is not a repository link: ${dest}`];
|
|
129
|
+
}
|
|
130
|
+
const hash = dest.indexOf("#");
|
|
131
|
+
let pathPart = hash >= 0 ? dest.slice(0, hash) : dest;
|
|
132
|
+
const frag = hash >= 0 ? dest.slice(hash + 1) : "";
|
|
133
|
+
pathPart = pathPart.split("?")[0];
|
|
134
|
+
let decodedPath = pathPart;
|
|
135
|
+
let decodedFrag = frag;
|
|
136
|
+
try { decodedPath = decodeURIComponent(pathPart); } catch { /* keep raw */ }
|
|
137
|
+
try { decodedFrag = decodeURIComponent(frag); } catch { /* keep raw */ }
|
|
138
|
+
if (!decodedPath) {
|
|
139
|
+
if (!decodedFrag) return [];
|
|
140
|
+
const ids = headingIds(readFileSync(file, "utf8"));
|
|
141
|
+
if (!ids.has(decodedFrag)) return [`${file}:${lineNumber}: missing anchor #${decodedFrag}`];
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
const target = resolve(dirname(file), decodedPath);
|
|
145
|
+
if (!existsSync(target)) return [`${file}:${lineNumber}: missing ${dest}`];
|
|
146
|
+
if (decodedFrag && target.endsWith(".md")) {
|
|
147
|
+
const ids = headingIds(readFileSync(target, "utf8"));
|
|
148
|
+
if (!ids.has(decodedFrag)) return [`${file}:${lineNumber}: missing anchor ${dest}`];
|
|
149
|
+
}
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function relatedProblems(file, markdown) {
|
|
154
|
+
if (!markdown.startsWith("---\n")) return [];
|
|
155
|
+
const end = markdown.indexOf("\n---", 4);
|
|
156
|
+
if (end < 0) return [];
|
|
157
|
+
const front = markdown.slice(4, end).split("\n");
|
|
158
|
+
const problems = [];
|
|
159
|
+
let inRelated = false;
|
|
160
|
+
for (let index = 0; index < front.length; index += 1) {
|
|
161
|
+
const line = front[index];
|
|
162
|
+
const inline = line.match(/^related:\s*\[(.*)\]\s*$/);
|
|
163
|
+
if (inline) {
|
|
164
|
+
for (const item of inline[1].split(",")) {
|
|
165
|
+
const rel = item.trim().replace(/^["']|["']$/g, "");
|
|
166
|
+
if (!rel || /^[a-z][a-z0-9+.-]*:/i.test(rel)) continue;
|
|
167
|
+
if (!existsSync(resolve(dirname(file), rel))) {
|
|
168
|
+
problems.push(`${file}:${index + 2}: missing related ${rel}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (/^related:\s*$/.test(line)) {
|
|
174
|
+
inRelated = true;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (inRelated && /^\S/.test(line)) inRelated = false;
|
|
178
|
+
if (!inRelated) continue;
|
|
179
|
+
const item = line.match(/^\s+-\s+(.+?)\s*$/);
|
|
180
|
+
if (!item) continue;
|
|
181
|
+
const rel = item[1].replace(/^["']|["']$/g, "");
|
|
182
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(rel)) continue;
|
|
183
|
+
if (!existsSync(resolve(dirname(file), rel))) {
|
|
184
|
+
problems.push(`${file}:${index + 2}: missing related ${rel}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return problems;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const linkRe = /(!?\[[^\]\n]*\])\(([^)\n]+)\)/g;
|
|
191
|
+
// Footnote bodies (`[^id]: prose`) are not link destinations. The URL inside
|
|
192
|
+
// the body is an ordinary inline link.
|
|
193
|
+
const refRe = /^ {0,3}\[([^\]^\n][^\]]*)\]:\s+(\S+)/;
|
|
194
|
+
|
|
195
|
+
export function checkFile(file) {
|
|
196
|
+
const markdown = readFileSync(file, "utf8");
|
|
197
|
+
const problems = relatedProblems(file, markdown);
|
|
198
|
+
const lines = visibleLines(markdown);
|
|
199
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
200
|
+
const line = lines[index];
|
|
201
|
+
for (const match of line.matchAll(linkRe)) {
|
|
202
|
+
problems.push(...problemsForDestination(file, index + 1, destinationOf(match[2])));
|
|
203
|
+
}
|
|
204
|
+
const ref = line.match(refRe);
|
|
205
|
+
if (ref) problems.push(...problemsForDestination(file, index + 1, destinationOf(ref[2])));
|
|
206
|
+
}
|
|
207
|
+
return problems;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function walk(dir, out = []) {
|
|
211
|
+
for (const name of readdirSync(dir)) {
|
|
212
|
+
const path = join(dir, name);
|
|
213
|
+
if (statSync(path).isDirectory()) walk(path, out);
|
|
214
|
+
else if (name.endsWith(".md")) out.push(path);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function checkPlans(root = repoRoot) {
|
|
220
|
+
const dir = join(root, "plans");
|
|
221
|
+
return walk(dir).flatMap((file) => checkFile(file));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function main() {
|
|
225
|
+
const problems = checkPlans();
|
|
226
|
+
for (const problem of problems) console.error(problem);
|
|
227
|
+
if (problems.length > 0) {
|
|
228
|
+
console.error(`check-plan-links: ${problems.length} broken`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const invoked = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
234
|
+
if (invoked) main();
|