@jrpool/kilotest 46.1.2 → 47.0.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.
Files changed (147) hide show
  1. package/.claude/settings.local.json +13 -1
  2. package/.devin/config.local.json +5 -1
  3. package/.github/workflows/ci.yml +0 -3
  4. package/.github/workflows/periodic-smoke-tests.yml +30 -0
  5. package/.github/workflows/publish-mcp.yml +4 -4
  6. package/{alerts.test.js → alerts.test.ts} +17 -29
  7. package/{alerts.js → alerts.ts} +10 -13
  8. package/api/{getReport.test.js → getReport.test.ts} +9 -10
  9. package/api/{getReport.js → getReport.ts} +23 -24
  10. package/api/{listDiagnoses.test.js → listDiagnoses.test.ts} +26 -27
  11. package/api/{listDiagnoses.js → listDiagnoses.ts} +46 -70
  12. package/api/{listIssues.test.js → listIssues.test.ts} +21 -14
  13. package/api/{listIssues.js → listIssues.ts} +75 -74
  14. package/api/{listReports.test.js → listReports.test.ts} +17 -16
  15. package/api/listReports.ts +87 -0
  16. package/api/{listViolators.test.js → listViolators.test.ts} +19 -20
  17. package/api/{listViolators.js → listViolators.ts} +45 -70
  18. package/api/requestFeature.test.ts +54 -0
  19. package/api/{requestFeature.js → requestFeature.ts} +18 -15
  20. package/api/{requestRetest.test.js → requestRetest.test.ts} +33 -28
  21. package/api/{requestRetest.js → requestRetest.ts} +32 -25
  22. package/api/{requestTest.test.js → requestTest.test.ts} +35 -30
  23. package/api/{requestTest.js → requestTest.ts} +35 -28
  24. package/api/{routes.test.js → routes.test.ts} +5 -5
  25. package/api/{routes.js → routes.ts} +6 -6
  26. package/api/{schemas.js → schemas.ts} +69 -50
  27. package/api/{util.test.js → util.test.ts} +31 -11
  28. package/api/{util.js → util.ts} +61 -36
  29. package/api/version.ts +8 -0
  30. package/{apiIntegration.test.js → apiIntegration.test.ts} +12 -13
  31. package/balances.ts +87 -0
  32. package/{checkinfo.js → checkinfo.ts} +24 -20
  33. package/docs/AI-TOOLS.md +4 -4
  34. package/docs/BACKLOG.md +356 -0
  35. package/docs/SERVICE.md +144 -17
  36. package/docs/decisions/002-coverage-tool.md +40 -0
  37. package/docs/decisions/003-adr-threshold.md +34 -0
  38. package/docs/decisions/004-docs-directory.md +23 -0
  39. package/docs/decisions/005-observability.md +28 -0
  40. package/docs/decisions/006-git-workflow.md +28 -0
  41. package/docs/decisions/007-internal-testing.md +34 -0
  42. package/docs/decisions/008-dependencies.md +28 -0
  43. package/docs/decisions/009-qai-integration.md +40 -0
  44. package/docs/decisions/QAI-HISTORICAL-CONTEXT.md +42 -0
  45. package/eslint.config.mjs +27 -0
  46. package/index.html +3 -3
  47. package/{index.test.js → index.test.ts} +380 -143
  48. package/{index.js → index.ts} +382 -283
  49. package/{indexBalances.test.js → indexBalances.test.ts} +21 -17
  50. package/{indexServe.test.js → indexServe.test.ts} +9 -9
  51. package/linkValidation.test.ts +190 -0
  52. package/{mcp.test.js → mcp.test.ts} +53 -26
  53. package/{mcp.js → mcp.ts} +29 -20
  54. package/memory/MEMORY.md +7 -0
  55. package/memory/user_maintainer_context.md +24 -0
  56. package/openapi.yaml +115 -68
  57. package/package.json +35 -15
  58. package/pm2.config.cjs +29 -0
  59. package/{routes.test.js → routes.test.ts} +8 -7
  60. package/{routing.test.js → routing.test.ts} +11 -12
  61. package/scripts/{generate-openapi.js → generate-openapi.ts} +13 -13
  62. package/serve.ts +15 -0
  63. package/smokeTest.ts +234 -0
  64. package/test/dbFixture.ts +21 -0
  65. package/test/fixtures/{buildFixtures.js → buildFixtures.ts} +14 -13
  66. package/test/{setup.cjs → setup.ts} +2 -2
  67. package/tsconfig.json +16 -0
  68. package/{util.test.js → util.test.ts} +350 -257
  69. package/util.ts +921 -0
  70. package/web/ai0BalanceForm/{ai0BalanceForm.test.js → ai0BalanceForm.test.ts} +15 -15
  71. package/web/ai0BalanceForm/{index.js → index.ts} +10 -18
  72. package/web/enqueue/index.ts +56 -0
  73. package/web/enqueueForm/index.html +9 -9
  74. package/web/enqueueForm/index.ts +43 -0
  75. package/web/expungeReportsForm/{expungeReportsForm.test.js → expungeReportsForm.test.ts} +18 -20
  76. package/web/expungeReportsForm/index.ts +18 -0
  77. package/web/hideReportForm/{hideReportForm.test.js → hideReportForm.test.ts} +14 -14
  78. package/web/hideReportForm/{index.js → index.ts} +17 -21
  79. package/web/listDiagnoses/{index.js → index.ts} +48 -58
  80. package/web/listDiagnoses/{listDiagnoses.test.js → listDiagnoses.test.ts} +39 -34
  81. package/web/listIssues/{index.js → index.ts} +67 -92
  82. package/web/listIssues/{listIssues.test.js → listIssues.test.ts} +45 -51
  83. package/web/listReports/index.html +3 -3
  84. package/web/listReports/{index.js → index.ts} +48 -53
  85. package/web/listReports/{listReports.test.js → listReports.test.ts} +41 -43
  86. package/web/listRules/{index.js → index.ts} +18 -23
  87. package/web/listRules/{listRules.test.js → listRules.test.ts} +26 -19
  88. package/web/listTopIssues/{index.js → index.ts} +36 -47
  89. package/web/listTopIssues/{listTopIssues.test.js → listTopIssues.test.ts} +26 -17
  90. package/web/listViolators/{index.js → index.ts} +51 -71
  91. package/web/listViolators/{listViolators.test.js → listViolators.test.ts} +43 -36
  92. package/web/manage/index.ts +22 -0
  93. package/web/pruneReportsForm/index.ts +18 -0
  94. package/web/pruneReportsForm/{pruneReportsForm.test.js → pruneReportsForm.test.ts} +11 -11
  95. package/web/reannotate/index.ts +70 -0
  96. package/web/reannotate/{reannotate.test.js → reannotate.test.ts} +19 -22
  97. package/web/reannotateForm/{index.js → index.ts} +41 -50
  98. package/web/reannotateForm/{reannotateForm.test.js → reannotateForm.test.ts} +13 -16
  99. package/web/renewWCAG/{index.js → index.ts} +9 -9
  100. package/web/renewWCAG/{renewWCAG.test.js → renewWCAG.test.ts} +17 -20
  101. package/web/renewWCAGForm/index.ts +22 -0
  102. package/web/{expungeReportsForm/index.js → reportDeletion.ts} +56 -38
  103. package/web/requestRetest/index.html +7 -7
  104. package/web/requestRetest/index.ts +31 -0
  105. package/web/requestRetest/requestRetest.test.ts +116 -0
  106. package/web/requestRetestForm/{index.js → index.ts} +10 -16
  107. package/web/requestRetestForm/{requestRetestForm.test.js → requestRetestForm.test.ts} +11 -12
  108. package/web/requestTest/index.html +7 -7
  109. package/web/requestTest/{index.js → index.ts} +5 -5
  110. package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +21 -21
  111. package/web/requestTestForm/index.html +4 -4
  112. package/web/requestTestForm/index.ts +21 -0
  113. package/web/requestTestForm/{requestTestForm.test.js → requestTestForm.test.ts} +9 -9
  114. package/web/rewindReportsForm/index.ts +18 -0
  115. package/web/rewindReportsForm/{rewindReportsForm.test.js → rewindReportsForm.test.ts} +11 -11
  116. package/web/tutorialAI/index.html +184 -0
  117. package/web/tutorialAI/index.ts +80 -0
  118. package/web/tutorialAI/tutorialAI.test.ts +192 -0
  119. package/web/{tutorial → tutorialWeb}/index.html +9 -8
  120. package/web/tutorialWeb/index.ts +78 -0
  121. package/web/tutorialWeb/tutorialWeb.test.ts +195 -0
  122. package/web/unhideReportForm/{index.js → index.ts} +17 -21
  123. package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
  124. package/aceconfig.js +0 -28
  125. package/api/listReports.js +0 -89
  126. package/api/requestFeature.test.js +0 -58
  127. package/api/version.js +0 -8
  128. package/indexFallback.test.js +0 -33
  129. package/indexWorkers.test.js +0 -79
  130. package/jsconfig.json +0 -8
  131. package/pm2.config.js +0 -18
  132. package/smokeTest.js +0 -128
  133. package/util.js +0 -744
  134. package/web/enqueue/index.js +0 -67
  135. package/web/enqueueForm/index.js +0 -49
  136. package/web/manage/index.js +0 -22
  137. package/web/pruneReportsForm/index.js +0 -123
  138. package/web/reannotate/index.js +0 -58
  139. package/web/renewWCAGForm/index.js +0 -22
  140. package/web/requestRetest/index.js +0 -31
  141. package/web/requestRetest/requestRetest.test.js +0 -119
  142. package/web/requestTestForm/index.js +0 -21
  143. package/web/rewindReportsForm/index.js +0 -121
  144. package/web/tutorial/index.js +0 -63
  145. package/web/tutorial/tutorial.test.js +0 -97
  146. /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
  147. /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
@@ -1,16 +1,16 @@
1
1
  /*
2
- index.js
2
+ index.ts
3
3
  Records a test request.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {isRecommendable, processTestRequest} = require('../../util');
8
+ import {getRequestability, processTestRequest} from '../../util.ts';
9
9
 
10
10
  // FUNCTIONS
11
11
 
12
- exports.answer = async (what, url, why) => {
13
- const status = await isRecommendable(url);
12
+ export const answer = async (description: string, url: string, why: string) => {
13
+ const status = await getRequestability(url);
14
14
  // If the target is already claimed or queued and is thus not requestable:
15
15
  if (status) {
16
16
  // Return an answer reporting this.
@@ -20,5 +20,5 @@ exports.answer = async (what, url, why) => {
20
20
  };
21
21
  }
22
22
  // Otherwise, i.e. if it is requestable, process the request.
23
- return await processTestRequest('test', __dirname, what, url, why);
23
+ return await processTestRequest('test', import.meta.dirname, description, url, why);
24
24
  };
@@ -1,20 +1,20 @@
1
1
  /*
2
- requestTest.test.js
3
- Unit tests for web/requestTest/index.js, covering the success path of answer.
2
+ requestTest.test.ts
3
+ Unit tests for web/requestTest/index.ts, covering the success path of answer.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {test, before, after, beforeEach} = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const fs = require('node:fs/promises');
11
- const path = require('node:path');
12
- const {parse} = require('node-html-parser');
8
+ import {test, before, after, beforeEach} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import fs from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import {parse} from 'node-html-parser';
13
13
 
14
14
  // CONSTANTS
15
15
 
16
- const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
17
- const recsPath = path.join(fixtureDBDir, 'jobs', 'recs.json');
16
+ import {fixtureDBDir} from '../../test/dbFixture.ts';
17
+ const testRequestsPath = path.join(fixtureDBDir, 'jobs', 'testRequests.json');
18
18
 
19
19
  // SETUP AND TEARDOWN
20
20
 
@@ -24,8 +24,8 @@ before(async () => {
24
24
  });
25
25
 
26
26
  beforeEach(async () => {
27
- // Reset recs.json and clean job directories before each test.
28
- await fs.writeFile(recsPath, '{}\n');
27
+ // Reset testRequests.json and clean job directories before each test.
28
+ await fs.writeFile(testRequestsPath, '{}\n');
29
29
  for (const sub of ['claimed', 'queue', 'failed']) {
30
30
  const dir = path.join(fixtureDBDir, 'jobs', sub);
31
31
  await fs.mkdir(dir, {recursive: true});
@@ -42,8 +42,8 @@ beforeEach(async () => {
42
42
  });
43
43
 
44
44
  after(async () => {
45
- // Restore recs.json and clean job directories after all tests.
46
- await fs.writeFile(recsPath, '{}\n');
45
+ // Restore testRequests.json and clean job directories after all tests.
46
+ await fs.writeFile(testRequestsPath, '{}\n');
47
47
  for (const sub of ['claimed', 'queue', 'failed']) {
48
48
  const dir = path.join(fixtureDBDir, 'jobs', sub);
49
49
  await fs.mkdir(dir, {recursive: true});
@@ -63,8 +63,8 @@ after(async () => {
63
63
 
64
64
  test('answer returns ok with a populated answer page for a valid test request', {timeout: 500}, async () => {
65
65
  // Require the module after DB_DIR is set.
66
- const {answer} = require('./index');
67
- const result = await answer(
66
+ const {answer} = await import('./index.ts');
67
+ const result: any = await answer(
68
68
  'Test Page', 'https://example.com/test-success', 'Because accessibility'
69
69
  );
70
70
  assert.equal(result.status, 'ok');
@@ -82,7 +82,7 @@ test('answer returns ok with a populated answer page for a valid test request',
82
82
  });
83
83
 
84
84
  test('answer returns an error when the URL is already queued', {timeout: 500}, async () => {
85
- const {answer} = require('./index');
85
+ const {answer} = await import('./index.ts');
86
86
  // Create a queued job with the target URL.
87
87
  const queueDir = path.join(fixtureDBDir, 'jobs', 'queue');
88
88
  const jobPath = path.join(queueDir, 'queuedJob.json');
@@ -91,7 +91,7 @@ test('answer returns an error when the URL is already queued', {timeout: 500}, a
91
91
  target: {what: 'Queued Page', url: 'https://example.com/queued'}
92
92
  }));
93
93
  try {
94
- const result = await answer(
94
+ const result: any = await answer(
95
95
  'Queued Page', 'https://example.com/queued', 'Because accessibility'
96
96
  );
97
97
  assert.equal(result.status, 'error');
@@ -107,9 +107,9 @@ test('answer returns an error when the URL is already queued', {timeout: 500}, a
107
107
  }
108
108
  });
109
109
 
110
- test('answer returns an error for an invalid recommendation', {timeout: 500}, async () => {
111
- const {answer} = require('./index');
112
- const result = await answer('', 'not-a-url', 'why');
110
+ test('answer returns an error for an invalid request', {timeout: 500}, async () => {
111
+ const {answer} = await import('./index.ts');
112
+ const result: any = await answer('', 'not-a-url', 'why');
113
113
  assert.equal(result.status, 'error');
114
- assert.equal(result.message, 'Invalid recommendation');
114
+ assert.equal(result.message, 'Invalid request');
115
115
  });
@@ -6,18 +6,18 @@
6
6
  <meta name="publisher" content="Jonathan Robert Pool">
7
7
  <meta name="creator" content="Jonathan Robert Pool">
8
8
  <meta name="keywords" content="report,accessibility,a11y">
9
- <title>Test recommendation | Kilotest</title>
9
+ <title>Test request | Kilotest</title>
10
10
  <link rel="icon" href="/favicon.ico">
11
11
  <link rel="stylesheet" href="/style.css">
12
12
  </head>
13
13
  <body>
14
14
  <main>
15
- <h1><a href="/">Kilotest</a>: Test recommendation</h1>
16
- <p>You may recommend a web page for testing by submitting the following form. All answers are required.</p>
15
+ <h1><a href="/">Kilotest</a>: Test request</h1>
16
+ <p>You may request that a web page be tested by submitting the following form. All answers are required.</p>
17
17
  <form action="/requestTest.html" method="post">
18
18
  <p><label>
19
19
  Propose a name for the page.<br>
20
- <input size="50" maxlength="70" name="what" required>
20
+ <input size="50" maxlength="70" name="description" required>
21
21
  </label></p>
22
22
  <p><label>
23
23
  What is the URL (starting with <code>https://</code>)?<br>
@@ -0,0 +1,21 @@
1
+ /*
2
+ index.ts
3
+ Serves a form for requesting a test.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import fs from 'node:fs/promises';
9
+ import path from 'node:path';
10
+
11
+ // FUNCTIONS
12
+
13
+ export const answer = async (): Promise<{status: string; answerPage: string}> => {
14
+ // Get the test request form template.
15
+ const answerPage = await fs.readFile(path.join(import.meta.dirname, 'index.html'), 'utf8');
16
+ // Return it.
17
+ return {
18
+ status: 'ok',
19
+ answerPage
20
+ };
21
+ };
@@ -1,14 +1,14 @@
1
1
  /*
2
- requestTestForm.test.js
3
- Unit tests for web/requestTestForm/index.js.
2
+ requestTestForm.test.ts
3
+ Unit tests for web/requestTestForm/index.ts.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {test} = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const {parse} = require('node-html-parser');
11
- const {answer} = require('./index');
8
+ import {test} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import {parse} from 'node-html-parser';
11
+ import {answer} from './index.ts';
12
12
 
13
13
  // TESTS
14
14
 
@@ -17,7 +17,7 @@ test('requestTestForm returns an ok status with valid HTML', async () => {
17
17
  assert.equal(result.status, 'ok');
18
18
  assert.ok(result.answerPage);
19
19
  const html = parse(result.answerPage);
20
- assert.equal(html.querySelector('title').text, 'Test recommendation | Kilotest');
20
+ assert.equal(html.querySelector('title')?.text, 'Test request | Kilotest');
21
21
  });
22
22
 
23
23
  test('requestTestForm includes a form that posts to /requestTest.html', async () => {
@@ -29,12 +29,12 @@ test('requestTestForm includes a form that posts to /requestTest.html', async ()
29
29
  assert.equal(form.getAttribute('method'), 'post');
30
30
  });
31
31
 
32
- test('requestTestForm includes required inputs for what, url, and why', async () => {
32
+ test('requestTestForm includes required inputs for description, url, and why', async () => {
33
33
  const result = await answer();
34
34
  const html = parse(result.answerPage);
35
35
  const inputs = html.querySelectorAll('input');
36
36
  const names = inputs.map(input => input.getAttribute('name'));
37
- assert.ok(names.includes('what'));
37
+ assert.ok(names.includes('description'));
38
38
  assert.ok(names.includes('url'));
39
39
  assert.ok(names.includes('why'));
40
40
  });
@@ -0,0 +1,18 @@
1
+ /*
2
+ index.ts
3
+ Serves a form for deleting latest superseding reports.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {reportDeletionForm} from '../reportDeletion.ts';
9
+
10
+ // FUNCTIONS
11
+
12
+ // Returns a form for deleting latest superseding reports.
13
+ export const answer = async (_: any, search: string) => reportDeletionForm(import.meta.dirname, search, {
14
+ intro: 'Choose the latest superseding reports to delete.',
15
+ emptyIntro: 'Each target has only 1 report, so there are no reports to delete.',
16
+ failurePrefix: 'Deleting latest superseding reports failed',
17
+ isDeletable: (specs, index) => specs[index - 1]?.url === specs[index]?.url && specs[index + 1]?.url !== specs[index]?.url
18
+ });
@@ -1,23 +1,23 @@
1
1
  /*
2
- rewindReportsForm.test.js
3
- Unit tests for web/rewindReportsForm/index.js.
2
+ rewindReportsForm.test.ts
3
+ Unit tests for web/rewindReportsForm/index.ts.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {test, before, after} = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const path = require('node:path');
11
- const fs = require('fs/promises');
12
- const {parse} = require('node-html-parser');
13
- const {answer} = require('./index');
14
- const {reportsPath} = require('../../util');
8
+ import {test, before, after} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import path from 'node:path';
11
+ import fs from 'fs/promises';
12
+ import {parse} from 'node-html-parser';
13
+ import {answer} from './index.ts';
14
+ import {reportsPath} from '../../util.ts';
15
15
 
16
16
  // SETUP AND TEARDOWN
17
17
 
18
18
  const savedAuthCode = process.env.AUTH_CODE;
19
19
  const savedDBDir = process.env.DB_DIR;
20
- const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
20
+ import {fixtureDBDir} from '../../test/dbFixture.ts';
21
21
 
22
22
  before(() => {
23
23
  process.env.AUTH_CODE = 'test-auth-code';
@@ -52,7 +52,7 @@ test('rewindReportsForm returns an error for an invalid auth code', async () =>
52
52
  test('rewindReportsForm returns an error when deleting a nonexistent report', async () => {
53
53
  const result = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
54
54
  assert.equal(result.status, 'error');
55
- assert.ok(result.message.includes('Deleting latest superseding reports'));
55
+ assert.ok(result.message?.includes('Deleting latest superseding reports'));
56
56
  });
57
57
 
58
58
  test('rewindReportsForm deletes a latest superseding report with valid auth code', async () => {
@@ -0,0 +1,184 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="publisher" content="Jonathan Robert Pool">
7
+ <meta name="creator" content="Jonathan Robert Pool">
8
+ <meta name="keywords" content="large language models,tools,model context protocol">
9
+ <meta name="description" content="Kilotest tutorial: how to connect an AI platform to Kilotest for technical analysis">
10
+ <title>Tutorial: Connect an AI Platform | Kilotest</title>
11
+ <link rel="icon" href="/favicon.ico">
12
+ <link rel="stylesheet" href="/style.css">
13
+ </head>
14
+ <body>
15
+ <main>
16
+ <h1>Connect an AI Platform to Kilotest</h1>
17
+ <h2>What you can do here</h2>
18
+ <ul>
19
+ <li>Do you ask questions to artificial intelligence (AI) assistants?</li>
20
+ <li>Do some of your questions ask for technical information?</li>
21
+ </ul>
22
+ <p>If you answered both of these questions with <q>yes</q>, this tutorial is designed for you.</p>
23
+ <h2>AI needs your help</h2>
24
+ <p>When you get AI help, you use AI platforms to interact with large language models (LLMs). LLMs are superb at understanding your questions, fetching information for you, and formulating answers from that information. But LLMs are <strong>not</strong> good at performing technical tasks to produce the accurate information they need. For those tasks, LLMs use <em>tools</em>. If you ask an LLM a question that requires a tool, you <strong>cannot assume</strong> that the LLM will know how to discover that tool. Typically, the LLM will try to answer you without the tool and will give you an inferior answer: superficial, vague, incomplete, inaccurate, or even fabricated. To ensure that the LLM uses the right tool, you can tell the AI platform to make the tool available to its LLMs.</p>
25
+ <p>This tutorial shows you how to do that.</p>
26
+ <h2>Let us begin with a concrete example</h2>
27
+ <p>Suppose the AI platform you want to use is Claude, and you will use it in a web browser.</p>
28
+ <p>Also suppose you will be asking an LLM on that platform to give you technical details about software quality: specifically, about the quality of a particular web page.</p>
29
+ <p>For that task, the LLM will need a tool that can test a web page to discover quality defects and report those defects to the LLM.</p>
30
+ <p>Finally, suppose you know about a tool that has that capability: Kilotest. You also know the URL that Kilotest makes available to LLMs: <code>https://kilotest.com/mcp</code>.</p>
31
+ <h2>What to do</h2>
32
+ <p>You can solve this problem with two steps.</p>
33
+ <h3>Step 1: Become a user</h3>
34
+ <p>If you are not already a user of the Claude AI platform, register yourself on it as a user. You can do this for free. Navigate to <a href="https://claude.ai/login">https://claude.ai/login</a> and use either Google or your email address to create an account. After you proceed through the substeps of creating a <q>Free plan</q> account, you are on the <a href="https://claude.ai/new">home page of a new task</a>.</p>
35
+ <h3>Step 2: Add a connector</h3>
36
+ <p>Now you are ready to add a connector to the Claude AI platform.</p>
37
+ <p>The page has a toolbar on the left side. Click it to expand it, so you can see labels, not only icons.</p>
38
+ <p>Now click the <q>Customize</q> tool.</p>
39
+ <p>A dialog opens. In the <q>Customize</q> list, click <q>Connectors</q>.</p>
40
+ <p>The dialog changes to a <q>Connectors</q> view.</p>
41
+ <p>In the <q>Add</q> dropdown list, click <q>Add custom connector</q>.</p>
42
+ <p>A subdialog opens, titled <q>Add custom connector</q>. It contains 4 inputs.</p>
43
+ <ul>
44
+ <li>Enter <q>Kilotest</q> into the <q>Name</q> input.</li>
45
+ <li>Enter <q>https://kilotest.com/mcp</q> into the <q>Remote MCP server URL</q> input.</li>
46
+ <li>Leave the OAuth inputs empty.</li>
47
+ <li>Submit the form by clicking the <q>Add</q> button.</li>
48
+ </ul>
49
+ <p>The dialog changes to a tool permissions view for the Kilotest connector. You can decide whether an LLM needs your approval to use each of the tools that Kilotest makes available.</p>
50
+ <p>Close the dialog by clicking the <code>x</code> button in the top-right corner or by pressing the <code>Escape</code> key.</p>
51
+ <h2>Finished!</h2>
52
+ <p>Now the platform will make the connector available to its LLMs.</p>
53
+ <p>Here is a simple test for you to verify that your configuration works:</p>
54
+ <ul>
55
+ <li>In the <q>How can I help you today?</q> input, choose which model you want to use.</li>
56
+ <li>Enter the question: <q>What quality issues exist on the World Wide Web Consortium home page?</q></li>
57
+ <li>Click the <q>Send message</q> up-arrow button or press the <code>Enter</code> key to submit your question.</li>
58
+ <li>From the answer that you get, determine whether the model used any Kilotest tools for this task.</li>
59
+ </ul>
60
+ <h2>Did this work?</h2>
61
+ <p>If the model used any Kilotest tools, congratulations! You know how to add tool power to the LLMs on one of the major AI platforms.</p>
62
+ <p>But what if it did <strong>not</strong> work? If an LLM fails to use the Kilotest connector when it should, or if you need to explicitly tell the LLM to use the connector, something is wrong, and we want to know about your experience. Please use the <a href="tutorialAIComment.html">comment form</a> to tell us about it.</p>
63
+ <p>You can also submit an issue on the <a href="https://github.com/jrpool/kilotest/issues">Kilotest GitHub repository</a>.</p>
64
+ <section id="suggest-improvement" class="main">
65
+ <h2>Suggest improvements</h2>
66
+ <p>Please submit suggestions for improving this tutorial below. The Kilotest managers will review them.</p>
67
+ <form id="comment-form" novalidate>
68
+ <p id="comment-label">Your suggestions (20–1000 characters):</p>
69
+ <textarea
70
+ id="comment-text"
71
+ name="content"
72
+ maxlength="1000"
73
+ minlength="20"
74
+ rows="5"
75
+ autocomplete="off"
76
+ aria-labelledby="comment-label"
77
+ aria-describedby="char-count-msg"
78
+ ></textarea>
79
+ <p class="char-count" id="char-count-msg"><span id="char-remaining">1000</span> characters remaining</p>
80
+ <button type="submit" id="comment-submit">Submit suggestions</button>
81
+ </form>
82
+ <div role="status" id="comment-diagnosis" aria-live="polite" aria-atomic="true" class="comment-diagnosis"></div>
83
+ </section>
84
+ </main>
85
+ <script>
86
+ const commentText = document.getElementById('comment-text');
87
+ const charRemaining = document.getElementById('char-remaining');
88
+ const charCountMsg = document.getElementById('char-count-msg');
89
+ const commentFeedback = document.getElementById('comment-diagnosis');
90
+ const commentSubmit = document.getElementById('comment-submit');
91
+
92
+ commentText.addEventListener('input', function () {
93
+ const remaining = 1000 - this.value.length;
94
+ charRemaining.textContent = remaining;
95
+ charCountMsg.className = remaining <= 100 ? 'char-count low' : 'char-count';
96
+ });
97
+
98
+ document.getElementById('comment-form').addEventListener('submit', async function (e) {
99
+ e.preventDefault();
100
+ const content = commentText.value.trim();
101
+ commentFeedback.className = 'comment-diagnosis';
102
+ commentFeedback.textContent = '';
103
+ if (!content) {
104
+ commentFeedback.className = 'comment-diagnosis error';
105
+ commentFeedback.textContent = 'Please enter your feedback before submitting.';
106
+ return;
107
+ }
108
+ commentSubmit.disabled = true;
109
+ try {
110
+ const response = await fetch('/tutorialAIComment.html', {
111
+ method: 'POST',
112
+ headers: {'Content-Type': 'application/json'},
113
+ body: JSON.stringify({content})
114
+ });
115
+ const data = await response.json();
116
+ if (data.status === 'ok') {
117
+ commentFeedback.className = 'comment-diagnosis success';
118
+ commentFeedback.textContent =
119
+ 'Thank you! Your suggestion has been saved and will be reviewed by the tutorial authors.';
120
+ this.reset();
121
+ charRemaining.textContent = '1000';
122
+ charCountMsg.className = 'char-count';
123
+ } else {
124
+ commentFeedback.className = 'comment-diagnosis error';
125
+ commentFeedback.textContent =
126
+ data.message || 'Your suggestion could not be saved. Please try again later.';
127
+ commentSubmit.disabled = false;
128
+ }
129
+ } catch (_) {
130
+ commentFeedback.className = 'comment-diagnosis error';
131
+ commentFeedback.textContent =
132
+ 'A connection error occurred. Please check your connection and try again.';
133
+ commentSubmit.disabled = false;
134
+ }
135
+ });
136
+ </script>
137
+ <style>
138
+ #suggest-improvement {
139
+ margin-top: 2rem;
140
+ padding-top: 2rem;
141
+ border-top: solid 1px #ccc;
142
+ }
143
+ #comment-form {
144
+ margin: 1rem 0;
145
+ }
146
+ #comment-form p {
147
+ margin: 0.5rem 0;
148
+ }
149
+ #comment-text {
150
+ width: 100%;
151
+ padding: 0.5rem;
152
+ border: solid 1px #ccc;
153
+ border-radius: 0.25rem;
154
+ font-family: system-ui, sans-serif;
155
+ font-size: 1rem;
156
+ }
157
+ .char-count {
158
+ font-size: 0.875rem;
159
+ color: #666;
160
+ margin: 0.25rem 0;
161
+ }
162
+ .char-count.low {
163
+ color: #c80;
164
+ font-weight: 700;
165
+ }
166
+ .comment-diagnosis {
167
+ margin-top: 0.75rem;
168
+ padding: 0.5rem 0.75rem;
169
+ border-radius: 0.25rem;
170
+ min-height: 1.5rem;
171
+ }
172
+ .comment-diagnosis.error {
173
+ background: #fdecea;
174
+ border-left: solid 4px #c0392b;
175
+ color: #5a1a1a;
176
+ }
177
+ .comment-diagnosis.success {
178
+ background: #e6f4ea;
179
+ border-left: solid 4px #2d7a2d;
180
+ color: #1a4a1a;
181
+ }
182
+ </style>
183
+ </body>
184
+ </html>
@@ -0,0 +1,80 @@
1
+ /*
2
+ index.ts
3
+ Serves the AI agent tutorial (Connect an AI Platform to Kilotest) and saves AI tutorial comments.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {sendAlert} from '../../alerts.ts';
9
+ import {checkCommentLength, getJSON} from '../../util.ts';
10
+ import fs from 'node:fs/promises';
11
+ import path from 'node:path';
12
+
13
+ // FUNCTIONS (helpers)
14
+
15
+ const getCommentsPath = () => process.env.TUTORIAL_AI_COMMENTS_PATH || path.join(import.meta.dirname, '../../db/comments/tutorialAI.json');
16
+
17
+ // FUNCTIONS
18
+
19
+ // Strips HTML tags and control characters, trims, and limits to 1000 characters.
20
+ const sanitize = (str: string) => str
21
+ .replace(/<[^>]*>/g, '')
22
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
23
+ .trim()
24
+ .slice(0, 1000);
25
+
26
+ // Returns the AI tutorial page.
27
+ export const answer = async () => {
28
+ const answerPage = await fs.readFile(path.join(import.meta.dirname, 'index.html'), 'utf8');
29
+ return {
30
+ status: 'ok',
31
+ answerPage
32
+ };
33
+ };
34
+
35
+ // Sanitizes and saves an AI tutorial comment to comments.json.
36
+ export const handleComment = async (content: unknown) => {
37
+ if (!content || typeof content !== 'string') {
38
+ return {status: 'error', message: 'No content provided'};
39
+ }
40
+ // Check length on the raw content.
41
+ const lengthCheck = checkCommentLength(content);
42
+ if (lengthCheck.status === 'error') {
43
+ return lengthCheck;
44
+ }
45
+ const commentsPath = getCommentsPath();
46
+ let comments: any[] = [];
47
+ try {
48
+ const existing = await fs.readFile(commentsPath, 'utf8');
49
+ comments = JSON.parse(existing);
50
+ }
51
+ catch {
52
+ // Initialize empty comments array.
53
+ }
54
+ const sanitized = sanitize(content);
55
+ if (!sanitized) {
56
+ return {status: 'error', message: 'Comment is empty after sanitization'};
57
+ }
58
+ // Check for duplicate submissions within the last 1000 seconds.
59
+ const duplicates = comments
60
+ .filter(comment => new Date(comment.dateTime).getTime() > Date.now() - 1000000)
61
+ .filter(comment => comment.content === sanitized);
62
+ if (duplicates.length) {
63
+ return {
64
+ status: 'error',
65
+ message: 'Your comment repeats a recently submitted one, but you are welcome to submit a different comment'
66
+ };
67
+ }
68
+ // Add the comment to the existing ones, using dateTime format.
69
+ comments.push({
70
+ dateTime: new Date().toISOString(),
71
+ content: sanitized
72
+ });
73
+ // Ensure the comments directory exists.
74
+ await fs.mkdir(path.dirname(commentsPath), {recursive: true});
75
+ // Save the revised comments.
76
+ await fs.writeFile(commentsPath, getJSON(comments));
77
+ // Send an alert to the manager.
78
+ await sendAlert('New AI tutorial comment received', 'A new AI tutorial comment has been received.');
79
+ return {status: 'ok'};
80
+ };