@jrpool/kilotest 63.0.0 → 63.1.1

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
@@ -97,5 +97,6 @@ When you **make an API or tool contract change** in a feature branch:
97
97
  8. Wait until `npmjs.com` has processed the publication.
98
98
  9. Use `git pull`, `npm ci`, and `pm2 restart kilotest` (but **not** `npm audit fix`) to deploy the revised `main` branch on the public server.
99
99
  10. Visit the [RapidAPI Studio](https://rapidapi.com/studio) and update the API definition at `Hub Listing` → `Definitions` → `CI/CD` → `Import OpenAPI` by uploading the regenerated `openapi-3.0.yaml`.
100
+ 11. Visit the [Claude directory](https://claude.ai/directory/manage/kilotest?tab=edit), edit any changed entries, and submit the changes.
100
101
 
101
102
  If any step fails, see the details in `docs/AI-TOOLS.md` for troubleshooting instructions.
package/docs/AI-TOOLS.md CHANGED
@@ -30,6 +30,7 @@ The internal features that make Kilotest a collection of tools for language mode
30
30
  - A `capability.md` file with a URL, summarizing what Kilotest tools can do.
31
31
  - `mcp`, `mcp-server`, and `modelcontextprotocol` keywords in `package.json`, supporting discovery of the `@jrpool/kilotest` npm package by tools and humans searching the npm registry for MCP servers.
32
32
  - An `lhm.plugin.json` file, conforming to the [LobeHub plugin manifest format](https://market.lobehub.com/s/publish-mcp/references/manifest), declaring Kilotest as a plugin in the [LobeHub MCP Market](https://lobehub.com/mcp). Publishing and updating are non-automated (`lhm plugin publish` for the initial listing; thereafter `npm run publish:lhm`, which wraps `lhm plugin update` and is run by `npm run postPR`), because LobeHub requires browser-based OAuth and does not support machine-to-machine publishing.
33
+ - Files in the `well-known` directory for ownership verification to AI registries, including OpenAI and Sealkeeper.
33
34
  - A `sitemap.xml` file.
34
35
  - Documentation in the `README.md` file.
35
36
  - This `AI-TOOLS.md` file.
@@ -49,6 +50,7 @@ The external features that support the use of Kilotest as a collection of AI too
49
50
  - Registration of Kilotest with [Awesome MCP Servers](https://www.tensorblock.co/mcp/servers/github-jrpool-kilotest-f518ba36).
50
51
  - Registration of Kilotest with the [RapidAPI](https://rapidapi.com/jrpool/api/kilotest/playground/apiendpoint_0f03577a-ff9a-472a-a0ed-533bd198981a) Hub.
51
52
  - Registration of Kilotest with the [LobeHub MCP Market](https://lobehub.com/mcp) (identifier: `jrpool-kilotest`), enabled with an `lhm.plugin.json` file.
53
+ - Registration of Kilotest with [MCPTry](https://mcptry.com/servers/io.github.jrpool/kilotest).
52
54
  - Deployment of an MCP server in HTTP mode on the Kilotest service host.
53
55
  - Submission of the MCP server to the [Claude directory of plugins and connectors](https://claude.ai/directory/manage).
54
56
  - Configuration of Claude Desktop on the local development host and the `claude.ai` web application to connect Claude Desktop models to the Kilotest MCP server. The configuration was performed in the UI of each platform with the addition of Kilotest as a _connector_. The user used the `Customize/Connectors/Add connector/Add custom connector` interface, providing these data before activating the `Add` button:
package/index.ts CHANGED
@@ -933,8 +933,7 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
933
933
  await serveError({message: 'Invalid service request'}, response, false);
934
934
  }
935
935
  }
936
- // Otherwise, if it is for a well-known resource (e.g. the OpenAI domain-verification
937
- // challenge token):
936
+ // Otherwise, if it is for a well-known resource:
938
937
  else if (pathname.startsWith('/.well-known/')) {
939
938
  const knownFile = pathname.slice('/.well-known/'.length);
940
939
  const knownPath = path.join(import.meta.dirname, 'well-known', knownFile);
package/lhm.plugin.json CHANGED
@@ -4818,5 +4818,5 @@
4818
4818
  }
4819
4819
  }
4820
4820
  ],
4821
- "version": "63.0.0"
4821
+ "version": "63.1.1"
4822
4822
  }
@@ -0,0 +1,47 @@
1
+ # Media
2
+
3
+ This directory holds the ChatGPT demonstration video, `demo-chatgpt.mp4`, which the web page `/demo-chatgpt.html` offers as an alternative to its step-through slideshow. The video is served at `/media/demo-chatgpt.mp4`.
4
+
5
+ ## Source
6
+
7
+ The video is built from `demo-chatgpt-src/`:
8
+
9
+ - `demo_list.txt` lists the slides in order. Each slide is a frame file followed by the number of seconds it stays on screen.
10
+ - `demo-chatgpt-src/labeled/` holds the 800×700 PNG frames that the list references.
11
+
12
+ The final line of `demo_list.txt` repeats the last frame without a duration. ffmpeg's concat format requires this line, and the build script handles it.
13
+
14
+ ## Changing durations
15
+
16
+ 1. Edit the `duration` lines in `demo-chatgpt-src/demo_list.txt`. Durations must be whole numbers of seconds, because the video has one frame per second.
17
+ 2. To change the slides themselves, add or replace frame files in `demo-chatgpt-src/labeled/` and update the `file` lines to match.
18
+ 3. Rebuild the video from the repository root:
19
+
20
+ ```bash
21
+ npm run build:media
22
+ ```
23
+
24
+ The script writes `media/demo-chatgpt.mp4`. Its length is the sum of the durations, and it is encoded at one frame per second.
25
+
26
+ To write the video somewhere else, pass a path:
27
+
28
+ ```bash
29
+ npm run build:media -- /path/to/output.mp4
30
+ ```
31
+
32
+ ## Requirements
33
+
34
+ - [ffmpeg](https://ffmpeg.org/) on the PATH. On macOS, install it with `brew install ffmpeg`.
35
+
36
+ ## Verification
37
+
38
+ The committed `demo-chatgpt.mp4` is byte-identical to the output of `npm run build:media` with the sources in this folder. After a rebuild, check that the video length matches the sum of the durations:
39
+
40
+ ```bash
41
+ ffprobe -v error -show_entries format=duration -of default=nw=1 media/demo-chatgpt.mp4
42
+ ```
43
+
44
+ ## Related files
45
+
46
+ - `scripts/buildDemoVideo.ts` contains the build script.
47
+ - `web/demo-chatgpt/` contains the slideshow version of the same demonstration, which is independent of this video.
@@ -0,0 +1,27 @@
1
+ file 'labeled/frame00.png'
2
+ duration 3
3
+ file 'labeled/frame01.png'
4
+ duration 12
5
+ file 'labeled/frame02.png'
6
+ duration 4
7
+ file 'labeled/frame03.png'
8
+ duration 10
9
+ file 'labeled/frame04.png'
10
+ duration 8
11
+ file 'labeled/frame05.png'
12
+ duration 11
13
+ file 'labeled/frame06.png'
14
+ duration 5
15
+ file 'labeled/frame07.png'
16
+ duration 25
17
+ file 'labeled/frame08.png'
18
+ duration 22
19
+ file 'labeled/frame09.png'
20
+ duration 14
21
+ file 'labeled/frame10.png'
22
+ duration 18
23
+ file 'labeled/frame11.png'
24
+ duration 26
25
+ file 'labeled/frame12.png'
26
+ duration 12
27
+ file 'labeled/frame12.png'
Binary file
package/openapi-3.0.yaml CHANGED
@@ -2,7 +2,7 @@ openapi: 3.0.3
2
2
  info:
3
3
  title: Kilotest API
4
4
  description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 12 rule engines to test web pages for front-end quality (accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.'
5
- version: 63.0.0
5
+ version: 63.1.1
6
6
  license:
7
7
  name: MIT
8
8
  url: https://github.com/jrpool/kilotest/blob/main/LICENSE
package/openapi.yaml CHANGED
@@ -2,7 +2,7 @@ openapi: 3.1.0
2
2
  info:
3
3
  title: Kilotest API
4
4
  description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 12 rule engines to test web pages for front-end quality (accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.'
5
- version: 63.0.0
5
+ version: 63.1.1
6
6
  license:
7
7
  name: MIT
8
8
  identifier: MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "63.0.0",
3
+ "version": "63.1.1",
4
4
  "description": "Ensemble testing for front-end web quality",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -15,6 +15,7 @@
15
15
  "test:smoke": "node smokeTest.ts",
16
16
  "checkinfo": "node checkinfo.ts",
17
17
  "generate": "node scripts/generate-openapi.ts && node scripts/update-versions.ts",
18
+ "build:media": "node scripts/buildDemoVideo.ts",
18
19
  "publish:lhm": "npx -y @lobehub/market-cli plugin update --dir .",
19
20
  "prePR": "node scripts/prePR.ts",
20
21
  "postPR": "node scripts/postPR.ts"
@@ -0,0 +1,121 @@
1
+ /*
2
+ buildDemoVideo.ts
3
+ Rebuilds the ChatGPT demonstration video, media/demo-chatgpt.mp4, from the slide frames and durations listed in media/demo-chatgpt-src/demo_list.txt. Each listed frame is shown for its duration in whole seconds. The video is encoded at one frame per second, which keeps the file small because the slides are static, and its length is set to the sum of the durations.
4
+
5
+ Usage:
6
+ npm run build:media [outputPath]
7
+
8
+ outputPath defaults to media/demo-chatgpt.mp4. Requires ffmpeg on the PATH, for example from Homebrew (brew install ffmpeg). See media/README.md.
9
+ */
10
+
11
+ // IMPORTS
12
+
13
+ import {spawnSync} from 'node:child_process';
14
+ import fs from 'node:fs';
15
+ import os from 'node:os';
16
+ import path from 'node:path';
17
+
18
+ // CONSTANTS
19
+
20
+ const repoDir = path.join(import.meta.dirname, '..');
21
+ const sourceDir = path.join(repoDir, 'media', 'demo-chatgpt-src');
22
+ const listPath = path.join(sourceDir, 'demo_list.txt');
23
+ const outputPath = path.resolve(process.argv[2] ?? path.join(repoDir, 'media', 'demo-chatgpt.mp4'));
24
+ // One frame per second suffices because durations are whole seconds and the slides do not animate.
25
+ const FRAME_RATE = 1;
26
+
27
+ // FUNCTIONS
28
+
29
+ // Reads the concat list into entries, each with a frame file name and, except for a final repeat, a duration in seconds.
30
+ const readEntries = (): {fileName: string; seconds?: number}[] => {
31
+ const entries: {fileName: string; seconds?: number}[] = [];
32
+ const lines = fs.readFileSync(listPath, 'utf8').split('\n').map(line => line.trim()).filter(line => line !== '');
33
+ for (const line of lines) {
34
+ const fileMatch = line.match(/^file '(.+)'$/);
35
+ const durationMatch = line.match(/^duration (\d+)$/);
36
+ const previous = entries.at(-1);
37
+ if (fileMatch) {
38
+ if (previous && previous.seconds === undefined) {
39
+ throw new Error(`No duration follows ${previous.fileName} in ${listPath}`);
40
+ }
41
+ entries.push({fileName: fileMatch[1] ?? ''});
42
+ }
43
+ else if (durationMatch && previous && previous.seconds === undefined) {
44
+ previous.seconds = Number(durationMatch[1]);
45
+ }
46
+ else {
47
+ throw new Error(`Unexpected line in ${listPath}: ${line}`);
48
+ }
49
+ }
50
+ return entries;
51
+ };
52
+
53
+ // Returns the slides to show, each with an absolute frame path and a duration. The final file may repeat without a duration, as the concat format requires, and must then repeat the file before it.
54
+ const readSlides = (): {framePath: string; seconds: number}[] => {
55
+ const entries = readEntries();
56
+ const last = entries.at(-1);
57
+ const timed = last?.seconds === undefined ? entries.slice(0, -1) : entries;
58
+ if (last?.seconds === undefined && last?.fileName !== timed.at(-1)?.fileName) {
59
+ throw new Error(`The final file without a duration must repeat the file before it in ${listPath}`);
60
+ }
61
+ const slides = timed.map(entry => ({
62
+ framePath: path.resolve(sourceDir, entry.fileName),
63
+ seconds: entry.seconds ?? 0
64
+ }));
65
+ if (!slides.length) {
66
+ throw new Error(`No slides found in ${listPath}`);
67
+ }
68
+ const missing = slides.filter(slide => !fs.existsSync(slide.framePath)).map(slide => slide.framePath);
69
+ if (missing.length) {
70
+ throw new Error(`Missing frame files:\n${missing.join('\n')}`);
71
+ }
72
+ return slides;
73
+ };
74
+
75
+ // Serializes slides as an ffmpeg concat list. The final frame is listed again without a duration, which ffmpeg needs to show it for its full duration.
76
+ const getConcatList = (slides: {framePath: string; seconds: number}[]): string => {
77
+ const lines = slides.flatMap(slide => [
78
+ `file '${slide.framePath.replaceAll("'", "'\\''")}'`,
79
+ `duration ${slide.seconds}`
80
+ ]);
81
+ const last = slides.at(-1);
82
+ if (last) {
83
+ lines.push(`file '${last.framePath.replaceAll("'", "'\\''")}'`);
84
+ }
85
+ return `${lines.join('\n')}\n`;
86
+ };
87
+
88
+ // Runs ffmpeg with the given arguments and throws on failure.
89
+ const runFfmpeg = (args: string[]): void => {
90
+ const result = spawnSync('ffmpeg', ['-v', 'error', '-y', ...args], {encoding: 'utf8'});
91
+ if (result.error) {
92
+ throw new Error('ffmpeg is not available. Install it (for example, with brew install ffmpeg) and try again.');
93
+ }
94
+ if (result.status !== 0) {
95
+ throw new Error(`ffmpeg failed: ${result.stderr.trim()}`);
96
+ }
97
+ };
98
+
99
+ // OPERATION
100
+
101
+ const slides = readSlides();
102
+ const totalSeconds = slides.reduce((sum, slide) => sum + slide.seconds, 0);
103
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'demo-chatgpt-'));
104
+ try {
105
+ const concatPath = path.join(tempDir, 'concat.txt');
106
+ fs.writeFileSync(concatPath, getConcatList(slides));
107
+ runFfmpeg([
108
+ '-f', 'concat',
109
+ '-safe', '0',
110
+ '-i', concatPath,
111
+ '-vf', `fps=${FRAME_RATE},format=yuv420p`,
112
+ '-t', String(totalSeconds),
113
+ '-c:v', 'libx264',
114
+ '-movflags', '+faststart',
115
+ outputPath
116
+ ]);
117
+ }
118
+ finally {
119
+ fs.rmSync(tempDir, {recursive: true, force: true});
120
+ }
121
+ console.log(`Wrote ${outputPath}: ${slides.length} slides, ${totalSeconds} seconds`);
package/server.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/jrpool/kilotest",
7
7
  "source": "github"
8
8
  },
9
- "version": "63.0.0",
9
+ "version": "63.1.1",
10
10
  "remotes": [
11
11
  {
12
12
  "type": "streamable-http",
@@ -14,5 +14,14 @@
14
14
  }
15
15
  ],
16
16
  "title": "Kilotest",
17
- "websiteUrl": "https://kilotest.com"
17
+ "websiteUrl": "https://kilotest.com",
18
+ "icons": [
19
+ {
20
+ "src": "https://kilotest.com/favicon.svg",
21
+ "mimeType": "image/svg+xml",
22
+ "sizes": [
23
+ "any"
24
+ ]
25
+ }
26
+ ]
18
27
  }
@@ -0,0 +1 @@
1
+ _N05CxIKKypvOFgBLNYjPw_BP5DHgMVPVv1mBFKU2lk