retestkit 1.4.1 → 1.5.0
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 +59 -40
- package/dist/config.js +8 -8
- package/dist/config.js.map +1 -1
- package/dist/logger.js +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +21 -21
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/templates/mcp/retest-crawl.md +7 -0
- package/{src/prompts/templates/mcp/webtest-discover-flows.md → dist/prompts/templates/mcp/retest-discover-flows.md} +1 -1
- package/{src/prompts/templates/mcp/webtest-discover.md → dist/prompts/templates/mcp/retest-discover.md} +2 -2
- package/dist/prompts/templates/mcp/retest-full-workflow.md +12 -0
- package/{src/prompts/templates/mcp/webtest-generate-tests.md → dist/prompts/templates/mcp/retest-generate-tests.md} +1 -1
- package/{src/prompts/templates/mcp/webtest-run-test.md → dist/prompts/templates/mcp/retest-run-test.md} +1 -1
- package/{src/prompts/templates/mcp/webtest-start.md → dist/prompts/templates/mcp/retest-start.md} +1 -1
- package/{src → dist}/prompts/templates/sampling/system-prefix.md +1 -1
- package/dist/resources/index.js +7 -7
- package/dist/resources/index.js.map +1 -1
- package/dist/schemas/config.js +2 -2
- package/dist/schemas/config.js.map +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/index.js.map +1 -1
- package/dist/server.js +3 -3
- package/dist/server.js.map +1 -1
- package/dist/test-utils/mock-context.js +22 -22
- package/dist/test-utils/mock-context.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -5
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/retest/crawl.d.ts.map +1 -0
- package/dist/tools/{webtest → retest}/crawl.js +7 -7
- package/dist/tools/retest/crawl.js.map +1 -0
- package/dist/tools/retest/discover-features.d.ts.map +1 -0
- package/dist/tools/{webtest → retest}/discover-features.js +6 -6
- package/dist/tools/retest/discover-features.js.map +1 -0
- package/dist/tools/retest/discover-flows.d.ts.map +1 -0
- package/dist/tools/{webtest → retest}/discover-flows.js +6 -6
- package/dist/tools/retest/discover-flows.js.map +1 -0
- package/dist/tools/retest/generate-tests.d.ts.map +1 -0
- package/dist/tools/{webtest → retest}/generate-tests.js +5 -5
- package/dist/tools/retest/generate-tests.js.map +1 -0
- package/dist/tools/retest/index.d.ts.map +1 -0
- package/dist/tools/retest/index.js.map +1 -0
- package/dist/tools/retest/run-test-case.d.ts.map +1 -0
- package/dist/tools/{webtest → retest}/run-test-case.js +3 -3
- package/dist/tools/retest/run-test-case.js.map +1 -0
- package/dist/tools/retest/schemas.d.ts.map +1 -0
- package/dist/tools/retest/schemas.js.map +1 -0
- package/dist/tools/retest/start-analysis.d.ts.map +1 -0
- package/dist/tools/{webtest → retest}/start-analysis.js +5 -5
- package/dist/tools/retest/start-analysis.js.map +1 -0
- package/dist/workspace/index.js +8 -8
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/types.d.ts +2 -2
- package/dist/workspace/types.d.ts.map +1 -1
- package/package.json +6 -2
- package/.claude/commands/openspec/apply.md +0 -23
- package/.claude/commands/openspec/archive.md +0 -27
- package/.claude/commands/openspec/proposal.md +0 -28
- package/.gemini/commands/openspec/apply.toml +0 -21
- package/.gemini/commands/openspec/archive.toml +0 -25
- package/.gemini/commands/openspec/proposal.toml +0 -26
- package/.github/prompts/openspec-apply.prompt.md +0 -22
- package/.github/prompts/openspec-archive.prompt.md +0 -26
- package/.github/prompts/openspec-proposal.prompt.md +0 -27
- package/.github/workflows/release.yml +0 -33
- package/.kilocode/workflows/openspec-apply.md +0 -17
- package/.kilocode/workflows/openspec-archive.md +0 -21
- package/.kilocode/workflows/openspec-proposal.md +0 -22
- package/.mcp.json +0 -23
- package/.opencode/command/openspec-apply.md +0 -25
- package/.opencode/command/openspec-archive.md +0 -28
- package/.opencode/command/openspec-proposal.md +0 -30
- package/.roo/commands/openspec-apply.md +0 -20
- package/.roo/commands/openspec-archive.md +0 -24
- package/.roo/commands/openspec-proposal.md +0 -25
- package/.vscode/mcp.json +0 -23
- package/AGENTS.md +0 -18
- package/CLAUDE.md +0 -18
- package/dist/tools/webtest/crawl.d.ts.map +0 -1
- package/dist/tools/webtest/crawl.js.map +0 -1
- package/dist/tools/webtest/discover-features.d.ts.map +0 -1
- package/dist/tools/webtest/discover-features.js.map +0 -1
- package/dist/tools/webtest/discover-flows.d.ts.map +0 -1
- package/dist/tools/webtest/discover-flows.js.map +0 -1
- package/dist/tools/webtest/generate-tests.d.ts.map +0 -1
- package/dist/tools/webtest/generate-tests.js.map +0 -1
- package/dist/tools/webtest/index.d.ts.map +0 -1
- package/dist/tools/webtest/index.js.map +0 -1
- package/dist/tools/webtest/run-test-case.d.ts.map +0 -1
- package/dist/tools/webtest/run-test-case.js.map +0 -1
- package/dist/tools/webtest/schemas.d.ts.map +0 -1
- package/dist/tools/webtest/schemas.js.map +0 -1
- package/dist/tools/webtest/start-analysis.d.ts.map +0 -1
- package/dist/tools/webtest/start-analysis.js.map +0 -1
- package/openspec/AGENTS.md +0 -456
- package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/proposal.md +0 -33
- package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/specs/webtest-resources/spec.md +0 -27
- package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/specs/webtest-tools/spec.md +0 -304
- package/openspec/changes/archive/2025-12-18-add-hybrid-artifact-paths/tasks.md +0 -43
- package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/design.md +0 -209
- package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/proposal.md +0 -41
- package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/specs/mcp-server-core/spec.md +0 -183
- package/openspec/changes/archive/2025-12-18-add-mcp-server-foundation/tasks.md +0 -112
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/design.md +0 -333
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/proposal.md +0 -66
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/mcp-server-core/spec.md +0 -129
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-lifecycle/spec.md +0 -138
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-logging/spec.md +0 -211
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-prompts/spec.md +0 -157
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-resources/spec.md +0 -213
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-sampling/spec.md +0 -257
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/specs/webtest-tools/spec.md +0 -501
- package/openspec/changes/archive/2025-12-18-add-webtest-orchestrator/tasks.md +0 -264
- package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/proposal.md +0 -24
- package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/specs/webtest-tools/spec.md +0 -80
- package/openspec/changes/archive/2025-12-18-allow-analysis-of-incomplete-crawls/tasks.md +0 -8
- package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/design.md +0 -90
- package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/proposal.md +0 -28
- package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/specs/webtest-sampling/spec.md +0 -90
- package/openspec/changes/archive/2025-12-18-fix-crawl-loop-stability/tasks.md +0 -33
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/design.md +0 -558
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/proposal.md +0 -119
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/specs/webtest-resources/spec.md +0 -109
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/specs/webtest-tools/spec.md +0 -121
- package/openspec/changes/archive/2025-12-18-use-markdown-artifacts/tasks.md +0 -133
- package/openspec/changes/extract-prompts-to-markdown/design.md +0 -86
- package/openspec/changes/extract-prompts-to-markdown/proposal.md +0 -50
- package/openspec/changes/extract-prompts-to-markdown/specs/webtest-prompts/spec.md +0 -74
- package/openspec/changes/extract-prompts-to-markdown/tasks.md +0 -40
- package/openspec/changes/refactor-webtest-naming/design.md +0 -95
- package/openspec/changes/refactor-webtest-naming/proposal.md +0 -66
- package/openspec/changes/refactor-webtest-naming/specs/webtest-prompts/spec.md +0 -79
- package/openspec/changes/refactor-webtest-naming/specs/webtest-resources/spec.md +0 -80
- package/openspec/changes/refactor-webtest-naming/specs/webtest-sampling/spec.md +0 -122
- package/openspec/changes/refactor-webtest-naming/specs/webtest-tools/spec.md +0 -113
- package/openspec/changes/refactor-webtest-naming/tasks.md +0 -119
- package/openspec/changes/rename-package-to-retest/proposal.md +0 -52
- package/openspec/changes/rename-package-to-retest/specs/mcp-server-core/spec.md +0 -53
- package/openspec/changes/rename-package-to-retest/specs/retest-lifecycle/spec.md +0 -68
- package/openspec/changes/rename-package-to-retest/specs/retest-logging/spec.md +0 -35
- package/openspec/changes/rename-package-to-retest/specs/retest-prompts/spec.md +0 -159
- package/openspec/changes/rename-package-to-retest/specs/retest-resources/spec.md +0 -251
- package/openspec/changes/rename-package-to-retest/specs/retest-sampling/spec.md +0 -99
- package/openspec/changes/rename-package-to-retest/specs/retest-tools/spec.md +0 -295
- package/openspec/changes/rename-package-to-retest/tasks.md +0 -71
- package/openspec/project.md +0 -31
- package/openspec/specs/mcp-server-core/spec.md +0 -178
- package/openspec/specs/webtest-lifecycle/spec.md +0 -136
- package/openspec/specs/webtest-logging/spec.md +0 -209
- package/openspec/specs/webtest-prompts/spec.md +0 -155
- package/openspec/specs/webtest-resources/spec.md +0 -248
- package/openspec/specs/webtest-sampling/spec.md +0 -344
- package/openspec/specs/webtest-tools/spec.md +0 -282
- package/release.config.js +0 -9
- package/src/config.test.ts +0 -96
- package/src/config.ts +0 -32
- package/src/elicitation/index.test.ts +0 -399
- package/src/elicitation/index.ts +0 -171
- package/src/elicitation/types.ts +0 -68
- package/src/index.ts +0 -83
- package/src/lifecycle/index.test.ts +0 -260
- package/src/lifecycle/index.ts +0 -101
- package/src/logger.redaction.test.ts +0 -322
- package/src/logger.test.ts +0 -123
- package/src/logger.ts +0 -229
- package/src/playwright-client/index.ts +0 -392
- package/src/playwright-client/types.ts +0 -99
- package/src/progress/index.test.ts +0 -327
- package/src/progress/index.ts +0 -170
- package/src/progress/types.ts +0 -25
- package/src/prompts/index.test.ts +0 -451
- package/src/prompts/index.ts +0 -246
- package/src/prompts/loader.test.ts +0 -100
- package/src/prompts/loader.ts +0 -59
- package/src/prompts/templates/mcp/webtest-crawl.md +0 -7
- package/src/prompts/templates/mcp/webtest-full-workflow.md +0 -12
- package/src/resources/index.ts +0 -250
- package/src/resources/subscriptions.ts +0 -37
- package/src/sampling/index.test.ts +0 -414
- package/src/sampling/index.ts +0 -286
- package/src/sampling/prompts.ts +0 -194
- package/src/sampling/types.ts +0 -60
- package/src/schemas/config.ts +0 -39
- package/src/security/index.test.ts +0 -441
- package/src/security/index.ts +0 -361
- package/src/security/security-scenarios.test.ts +0 -468
- package/src/server.ts +0 -211
- package/src/test-utils/index.ts +0 -6
- package/src/test-utils/mock-context.ts +0 -426
- package/src/test-utils/mock-playwright-client.ts +0 -422
- package/src/tools/index.ts +0 -11
- package/src/tools/webtest/crawl.test.ts +0 -834
- package/src/tools/webtest/crawl.ts +0 -901
- package/src/tools/webtest/discover-features.ts +0 -412
- package/src/tools/webtest/discover-flows.ts +0 -408
- package/src/tools/webtest/generate-tests.test.ts +0 -532
- package/src/tools/webtest/generate-tests.ts +0 -425
- package/src/tools/webtest/index.ts +0 -7
- package/src/tools/webtest/integration.test.ts +0 -536
- package/src/tools/webtest/run-test-case.test.ts +0 -659
- package/src/tools/webtest/run-test-case.ts +0 -508
- package/src/tools/webtest/schemas.ts +0 -201
- package/src/tools/webtest/start-analysis.test.ts +0 -151
- package/src/tools/webtest/start-analysis.ts +0 -158
- package/src/transports/http.ts +0 -19
- package/src/transports/index.ts +0 -30
- package/src/transports/stdio.ts +0 -7
- package/src/types/capabilities.test.ts +0 -193
- package/src/types/capabilities.ts +0 -50
- package/src/types/context.ts +0 -21
- package/src/types/tool.ts +0 -11
- package/src/workspace/index.ts +0 -945
- package/src/workspace/markdown.ts +0 -272
- package/src/workspace/types.ts +0 -186
- package/tests/integration/server.test.ts +0 -89
- package/tests/integration/tools.test.ts +0 -99
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -9
- package/vitest.integration.config.ts +0 -10
- /package/{src → dist}/prompts/templates/sampling/crawl-action.md +0 -0
- /package/{src → dist}/prompts/templates/sampling/feature-discovery.md +0 -0
- /package/{src → dist}/prompts/templates/sampling/flow-discovery.md +0 -0
- /package/{src → dist}/prompts/templates/sampling/page-content-wrapper.md +0 -0
- /package/{src → dist}/prompts/templates/sampling/test-evaluation.md +0 -0
- /package/{src → dist}/prompts/templates/sampling/test-generation.md +0 -0
- /package/dist/tools/{webtest → retest}/crawl.d.ts +0 -0
- /package/dist/tools/{webtest → retest}/discover-features.d.ts +0 -0
- /package/dist/tools/{webtest → retest}/discover-flows.d.ts +0 -0
- /package/dist/tools/{webtest → retest}/generate-tests.d.ts +0 -0
- /package/dist/tools/{webtest → retest}/index.d.ts +0 -0
- /package/dist/tools/{webtest → retest}/index.js +0 -0
- /package/dist/tools/{webtest → retest}/run-test-case.d.ts +0 -0
- /package/dist/tools/{webtest → retest}/schemas.d.ts +0 -0
- /package/dist/tools/{webtest → retest}/schemas.js +0 -0
- /package/dist/tools/{webtest → retest}/start-analysis.d.ts +0 -0
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { createStartAnalysisTool } from "./start-analysis.js";
|
|
3
|
-
import {
|
|
4
|
-
createMockContext,
|
|
5
|
-
type MockContext,
|
|
6
|
-
} from "../../test-utils/index.js";
|
|
7
|
-
|
|
8
|
-
describe("webtest_init", () => {
|
|
9
|
-
let context: MockContext;
|
|
10
|
-
let tool: ReturnType<typeof createStartAnalysisTool>;
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
context = createMockContext();
|
|
14
|
-
tool = createStartAnalysisTool(() => context as any);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe("tool metadata", () => {
|
|
18
|
-
it("has correct name", () => {
|
|
19
|
-
expect(tool.name).toBe("webtest_init");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("has a description", () => {
|
|
23
|
-
expect(tool.description).toBeDefined();
|
|
24
|
-
expect(tool.description.length).toBeGreaterThan(0);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("has an input schema", () => {
|
|
28
|
-
expect(tool.inputSchema).toBeDefined();
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("handler", () => {
|
|
33
|
-
it("creates workspace for valid URL", async () => {
|
|
34
|
-
const result = await tool.handler({
|
|
35
|
-
url: "https://example.com",
|
|
36
|
-
focus: "Test the login flow",
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
expect(result.isError).toBeFalsy();
|
|
40
|
-
expect(context.workspaceManager.createWorkspace).toHaveBeenCalledWith(
|
|
41
|
-
expect.objectContaining({
|
|
42
|
-
url: "https://example.com",
|
|
43
|
-
focus: "Test the login flow",
|
|
44
|
-
})
|
|
45
|
-
);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("returns analysisId in response", async () => {
|
|
49
|
-
const result = await tool.handler({
|
|
50
|
-
url: "https://example.com",
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
expect(result.isError).toBeFalsy();
|
|
54
|
-
const content = result.content[0];
|
|
55
|
-
expect(content.type).toBe("text");
|
|
56
|
-
expect(content.text).toContain("analysisId");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("uses default limits from config", async () => {
|
|
60
|
-
await tool.handler({
|
|
61
|
-
url: "https://example.com",
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
expect(context.workspaceManager.createWorkspace).toHaveBeenCalledWith(
|
|
65
|
-
expect.objectContaining({
|
|
66
|
-
limits: expect.objectContaining({
|
|
67
|
-
maxSteps: context.config.defaultMaxSteps,
|
|
68
|
-
maxMinutes: context.config.defaultMaxMinutes,
|
|
69
|
-
maxPages: context.config.defaultMaxPages,
|
|
70
|
-
}),
|
|
71
|
-
})
|
|
72
|
-
);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("respects custom limits", async () => {
|
|
76
|
-
await tool.handler({
|
|
77
|
-
url: "https://example.com",
|
|
78
|
-
limits: {
|
|
79
|
-
maxSteps: 10,
|
|
80
|
-
maxMinutes: 5,
|
|
81
|
-
maxPages: 3,
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
expect(context.workspaceManager.createWorkspace).toHaveBeenCalledWith(
|
|
86
|
-
expect.objectContaining({
|
|
87
|
-
limits: expect.objectContaining({
|
|
88
|
-
maxSteps: 10,
|
|
89
|
-
maxMinutes: 5,
|
|
90
|
-
maxPages: 3,
|
|
91
|
-
}),
|
|
92
|
-
})
|
|
93
|
-
);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("stores allowed domains from input URL", async () => {
|
|
97
|
-
const result = await tool.handler({
|
|
98
|
-
url: "https://shop.example.com/products",
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
expect(result.isError).toBeFalsy();
|
|
102
|
-
// The domain should be extracted and stored
|
|
103
|
-
const content = JSON.parse(result.content[0].text!);
|
|
104
|
-
expect(content.domain).toBe("shop.example.com");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("handles custom allowed domains", async () => {
|
|
108
|
-
const result = await tool.handler({
|
|
109
|
-
url: "https://shop.example.com",
|
|
110
|
-
allowedDomains: ["shop.example.com", "api.example.com"],
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
expect(result.isError).toBeFalsy();
|
|
114
|
-
const content = JSON.parse(result.content[0].text!);
|
|
115
|
-
expect(content.allowedDomains).toContain("shop.example.com");
|
|
116
|
-
expect(content.allowedDomains).toContain("api.example.com");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("returns error for invalid URL", async () => {
|
|
120
|
-
const result = await tool.handler({
|
|
121
|
-
url: "not-a-valid-url",
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
expect(result.isError).toBe(true);
|
|
125
|
-
expect(result.content[0].text).toContain("Error");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("logs analysis start", async () => {
|
|
129
|
-
await tool.handler({
|
|
130
|
-
url: "https://example.com",
|
|
131
|
-
focus: "Test flow",
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
expect(context.logger.info).toHaveBeenCalledWith(
|
|
135
|
-
expect.stringContaining("analysis"),
|
|
136
|
-
expect.any(Object)
|
|
137
|
-
);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("returns workspace URIs", async () => {
|
|
141
|
-
const result = await tool.handler({
|
|
142
|
-
url: "https://example.com",
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
expect(result.isError).toBeFalsy();
|
|
146
|
-
const content = JSON.parse(result.content[0].text!);
|
|
147
|
-
expect(content.workspaceRootUri).toContain("webtest://");
|
|
148
|
-
expect(content.statusUri).toContain("webtest://");
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
});
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { McpTool, ToolResult } from "../../types/tool.js";
|
|
3
|
-
import type { ServerContext } from "../../types/context.js";
|
|
4
|
-
import { AnalysisLimitsSchema } from "./schemas.js";
|
|
5
|
-
|
|
6
|
-
export const startAnalysisInputSchema = z.object({
|
|
7
|
-
url: z.string().url("Must be a valid URL"),
|
|
8
|
-
focus: z
|
|
9
|
-
.string()
|
|
10
|
-
.optional()
|
|
11
|
-
.describe("Optional focus area for the analysis (e.g., 'checkout flow', 'user registration')"),
|
|
12
|
-
limits: AnalysisLimitsSchema.optional().describe(
|
|
13
|
-
"Optional limits for the analysis"
|
|
14
|
-
),
|
|
15
|
-
allowedDomains: z
|
|
16
|
-
.array(z.string())
|
|
17
|
-
.optional()
|
|
18
|
-
.describe("Additional domains to allow during crawling (target domain is always allowed)"),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
export type StartAnalysisInput = z.infer<typeof startAnalysisInputSchema>;
|
|
22
|
-
|
|
23
|
-
export function createStartAnalysisTool(
|
|
24
|
-
getContext: () => ServerContext
|
|
25
|
-
): McpTool<StartAnalysisInput> {
|
|
26
|
-
return {
|
|
27
|
-
name: "webtest_init",
|
|
28
|
-
description: `Initialize a new web testing analysis workspace.
|
|
29
|
-
|
|
30
|
-
This tool creates a workspace for analyzing a web application. It:
|
|
31
|
-
- Validates the target URL
|
|
32
|
-
- Creates a workspace directory structure
|
|
33
|
-
- Records analysis metadata and limits
|
|
34
|
-
- Returns identifiers for subsequent operations
|
|
35
|
-
|
|
36
|
-
Use this as the first step before crawling, analyzing, or testing.`,
|
|
37
|
-
|
|
38
|
-
inputSchema: startAnalysisInputSchema,
|
|
39
|
-
|
|
40
|
-
async handler(input: StartAnalysisInput): Promise<ToolResult> {
|
|
41
|
-
const ctx = getContext();
|
|
42
|
-
const { config, logger } = ctx;
|
|
43
|
-
|
|
44
|
-
logger.info("Starting analysis", { url: input.url, focus: input.focus });
|
|
45
|
-
|
|
46
|
-
// Validate and normalize URL
|
|
47
|
-
let targetUrl: URL;
|
|
48
|
-
try {
|
|
49
|
-
targetUrl = new URL(input.url);
|
|
50
|
-
} catch {
|
|
51
|
-
return {
|
|
52
|
-
content: [
|
|
53
|
-
{
|
|
54
|
-
type: "text",
|
|
55
|
-
text: `Error: Invalid URL "${input.url}". Please provide a valid HTTP or HTTPS URL.`,
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
isError: true,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (!["http:", "https:"].includes(targetUrl.protocol)) {
|
|
63
|
-
return {
|
|
64
|
-
content: [
|
|
65
|
-
{
|
|
66
|
-
type: "text",
|
|
67
|
-
text: `Error: URL must use HTTP or HTTPS protocol. Got: ${targetUrl.protocol}`,
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
isError: true,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Determine limits
|
|
75
|
-
const limits = {
|
|
76
|
-
maxSteps: input.limits?.maxSteps ?? config.defaultMaxSteps,
|
|
77
|
-
maxMinutes: input.limits?.maxMinutes ?? config.defaultMaxMinutes,
|
|
78
|
-
maxPages: input.limits?.maxPages ?? config.defaultMaxPages,
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
// Build allowed domains list
|
|
82
|
-
const allowedDomains = new Set<string>([targetUrl.hostname]);
|
|
83
|
-
if (input.allowedDomains) {
|
|
84
|
-
for (const domain of input.allowedDomains) {
|
|
85
|
-
allowedDomains.add(domain);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Create workspace using workspace manager from context
|
|
90
|
-
// Note: We need to get this from an extended context that includes the workspace manager
|
|
91
|
-
const workspaceManager = (ctx as any).workspaceManager;
|
|
92
|
-
if (!workspaceManager) {
|
|
93
|
-
return {
|
|
94
|
-
content: [
|
|
95
|
-
{
|
|
96
|
-
type: "text",
|
|
97
|
-
text: "Error: Workspace manager not available. Server may not be fully initialized.",
|
|
98
|
-
},
|
|
99
|
-
],
|
|
100
|
-
isError: true,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const { analysisId, workspacePath } = await workspaceManager.createWorkspace({
|
|
106
|
-
url: input.url,
|
|
107
|
-
focus: input.focus,
|
|
108
|
-
limits,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Notify clients about new resource
|
|
112
|
-
const resourceManager = (ctx as any).resourceManager;
|
|
113
|
-
if (resourceManager) {
|
|
114
|
-
await resourceManager.notifyListChanged();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const result = {
|
|
118
|
-
analysisId,
|
|
119
|
-
workspaceRootPath: workspacePath,
|
|
120
|
-
workspaceRootUri: `webtest://${analysisId}`,
|
|
121
|
-
statusUri: `webtest://${analysisId}/index.md`,
|
|
122
|
-
domain: targetUrl.hostname,
|
|
123
|
-
allowedDomains: Array.from(allowedDomains),
|
|
124
|
-
limits,
|
|
125
|
-
nextSteps: [
|
|
126
|
-
`Use webtest_crawl_app with analysisId="${analysisId}" to explore the application`,
|
|
127
|
-
"Set a goal describing what you want to explore or test",
|
|
128
|
-
"After crawling, use webtest_analyze_app to analyze the application structure",
|
|
129
|
-
],
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
logger.info("Analysis workspace created", { analysisId, url: input.url });
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
content: [
|
|
136
|
-
{
|
|
137
|
-
type: "text",
|
|
138
|
-
text: JSON.stringify(result, null, 2),
|
|
139
|
-
},
|
|
140
|
-
],
|
|
141
|
-
};
|
|
142
|
-
} catch (error) {
|
|
143
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
144
|
-
logger.error("Failed to create analysis workspace", { error: message });
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
content: [
|
|
148
|
-
{
|
|
149
|
-
type: "text",
|
|
150
|
-
text: `Error creating workspace: ${message}`,
|
|
151
|
-
},
|
|
152
|
-
],
|
|
153
|
-
isError: true,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
}
|
package/src/transports/http.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2
|
-
import type { Logger } from "../logger.js";
|
|
3
|
-
|
|
4
|
-
export interface HttpTransportOptions {
|
|
5
|
-
port: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function createHttpTransport(
|
|
9
|
-
options: HttpTransportOptions,
|
|
10
|
-
logger: Logger
|
|
11
|
-
): StreamableHTTPServerTransport {
|
|
12
|
-
logger.info("Creating HTTP transport", { port: options.port });
|
|
13
|
-
|
|
14
|
-
return new StreamableHTTPServerTransport({
|
|
15
|
-
sessionIdGenerator: () => crypto.randomUUID(),
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export { StreamableHTTPServerTransport };
|
package/src/transports/index.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
2
|
-
import type { Config } from "../config.js";
|
|
3
|
-
import type { Logger } from "../logger.js";
|
|
4
|
-
import { createStdioTransport } from "./stdio.js";
|
|
5
|
-
import {
|
|
6
|
-
createHttpTransport,
|
|
7
|
-
StreamableHTTPServerTransport,
|
|
8
|
-
} from "./http.js";
|
|
9
|
-
|
|
10
|
-
export type TransportResult =
|
|
11
|
-
| { type: "stdio"; transport: Transport }
|
|
12
|
-
| { type: "http"; transport: StreamableHTTPServerTransport; port: number };
|
|
13
|
-
|
|
14
|
-
export function createTransport(config: Config, logger: Logger): TransportResult {
|
|
15
|
-
switch (config.transport) {
|
|
16
|
-
case "stdio":
|
|
17
|
-
return {
|
|
18
|
-
type: "stdio",
|
|
19
|
-
transport: createStdioTransport(logger),
|
|
20
|
-
};
|
|
21
|
-
case "http":
|
|
22
|
-
return {
|
|
23
|
-
type: "http",
|
|
24
|
-
transport: createHttpTransport({ port: config.port }, logger),
|
|
25
|
-
port: config.port,
|
|
26
|
-
};
|
|
27
|
-
default:
|
|
28
|
-
throw new Error(`Unknown transport type: ${config.transport}`);
|
|
29
|
-
}
|
|
30
|
-
}
|
package/src/transports/stdio.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
-
import type { Logger } from "../logger.js";
|
|
3
|
-
|
|
4
|
-
export function createStdioTransport(logger: Logger): StdioServerTransport {
|
|
5
|
-
logger.info("Creating stdio transport");
|
|
6
|
-
return new StdioServerTransport();
|
|
7
|
-
}
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
DEFAULT_CAPABILITIES,
|
|
4
|
-
hasSampling,
|
|
5
|
-
hasElicitation,
|
|
6
|
-
hasLogging,
|
|
7
|
-
hasProgress,
|
|
8
|
-
hasResourcesListChanged,
|
|
9
|
-
hasResourcesSubscribe,
|
|
10
|
-
isElicitationSupported,
|
|
11
|
-
type ClientCapabilities,
|
|
12
|
-
} from "./capabilities.js";
|
|
13
|
-
|
|
14
|
-
describe("capabilities", () => {
|
|
15
|
-
describe("DEFAULT_CAPABILITIES", () => {
|
|
16
|
-
it("has all capabilities disabled by default", () => {
|
|
17
|
-
expect(DEFAULT_CAPABILITIES.sampling).toBe(false);
|
|
18
|
-
expect(DEFAULT_CAPABILITIES.elicitation).toBe(false);
|
|
19
|
-
expect(DEFAULT_CAPABILITIES.logging).toBe(false);
|
|
20
|
-
expect(DEFAULT_CAPABILITIES.progress).toBe(false);
|
|
21
|
-
expect(DEFAULT_CAPABILITIES.resourcesListChanged).toBe(false);
|
|
22
|
-
expect(DEFAULT_CAPABILITIES.resourcesSubscribe).toBe(false);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("has unknown protocol version by default", () => {
|
|
26
|
-
expect(DEFAULT_CAPABILITIES.protocolVersion).toBe("unknown");
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("capability helper functions", () => {
|
|
31
|
-
const enabledCaps: ClientCapabilities = {
|
|
32
|
-
sampling: true,
|
|
33
|
-
elicitation: true,
|
|
34
|
-
logging: true,
|
|
35
|
-
progress: true,
|
|
36
|
-
resourcesListChanged: true,
|
|
37
|
-
resourcesSubscribe: true,
|
|
38
|
-
protocolVersion: "2025-06-18",
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const disabledCaps: ClientCapabilities = {
|
|
42
|
-
sampling: false,
|
|
43
|
-
elicitation: false,
|
|
44
|
-
logging: false,
|
|
45
|
-
progress: false,
|
|
46
|
-
resourcesListChanged: false,
|
|
47
|
-
resourcesSubscribe: false,
|
|
48
|
-
protocolVersion: "2024-01-01",
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
describe("hasSampling", () => {
|
|
52
|
-
it("returns true when sampling is enabled", () => {
|
|
53
|
-
expect(hasSampling(enabledCaps)).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("returns false when sampling is disabled", () => {
|
|
57
|
-
expect(hasSampling(disabledCaps)).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe("hasElicitation", () => {
|
|
62
|
-
it("returns true when elicitation is enabled", () => {
|
|
63
|
-
expect(hasElicitation(enabledCaps)).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("returns false when elicitation is disabled", () => {
|
|
67
|
-
expect(hasElicitation(disabledCaps)).toBe(false);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe("hasLogging", () => {
|
|
72
|
-
it("returns true when logging is enabled", () => {
|
|
73
|
-
expect(hasLogging(enabledCaps)).toBe(true);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("returns false when logging is disabled", () => {
|
|
77
|
-
expect(hasLogging(disabledCaps)).toBe(false);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe("hasProgress", () => {
|
|
82
|
-
it("returns true when progress is enabled", () => {
|
|
83
|
-
expect(hasProgress(enabledCaps)).toBe(true);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("returns false when progress is disabled", () => {
|
|
87
|
-
expect(hasProgress(disabledCaps)).toBe(false);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe("hasResourcesListChanged", () => {
|
|
92
|
-
it("returns true when resourcesListChanged is enabled", () => {
|
|
93
|
-
expect(hasResourcesListChanged(enabledCaps)).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("returns false when resourcesListChanged is disabled", () => {
|
|
97
|
-
expect(hasResourcesListChanged(disabledCaps)).toBe(false);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe("hasResourcesSubscribe", () => {
|
|
102
|
-
it("returns true when resourcesSubscribe is enabled", () => {
|
|
103
|
-
expect(hasResourcesSubscribe(enabledCaps)).toBe(true);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("returns false when resourcesSubscribe is disabled", () => {
|
|
107
|
-
expect(hasResourcesSubscribe(disabledCaps)).toBe(false);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe("isElicitationSupported", () => {
|
|
113
|
-
it("returns true when elicitation enabled and protocol version sufficient", () => {
|
|
114
|
-
const caps: ClientCapabilities = {
|
|
115
|
-
...DEFAULT_CAPABILITIES,
|
|
116
|
-
elicitation: true,
|
|
117
|
-
protocolVersion: "2025-06-18",
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
expect(isElicitationSupported(caps)).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("returns true for newer protocol versions", () => {
|
|
124
|
-
const caps: ClientCapabilities = {
|
|
125
|
-
...DEFAULT_CAPABILITIES,
|
|
126
|
-
elicitation: true,
|
|
127
|
-
protocolVersion: "2025-12-01",
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
expect(isElicitationSupported(caps)).toBe(true);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("returns false when protocol version is too old", () => {
|
|
134
|
-
const caps: ClientCapabilities = {
|
|
135
|
-
...DEFAULT_CAPABILITIES,
|
|
136
|
-
elicitation: true,
|
|
137
|
-
protocolVersion: "2025-01-01",
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
expect(isElicitationSupported(caps)).toBe(false);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("returns false when protocol version is unknown", () => {
|
|
144
|
-
const caps: ClientCapabilities = {
|
|
145
|
-
...DEFAULT_CAPABILITIES,
|
|
146
|
-
elicitation: true,
|
|
147
|
-
protocolVersion: "unknown",
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
expect(isElicitationSupported(caps)).toBe(false);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("returns false when elicitation is disabled", () => {
|
|
154
|
-
const caps: ClientCapabilities = {
|
|
155
|
-
...DEFAULT_CAPABILITIES,
|
|
156
|
-
elicitation: false,
|
|
157
|
-
protocolVersion: "2025-06-18",
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
expect(isElicitationSupported(caps)).toBe(false);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("returns false when both elicitation disabled and old protocol", () => {
|
|
164
|
-
const caps: ClientCapabilities = {
|
|
165
|
-
...DEFAULT_CAPABILITIES,
|
|
166
|
-
elicitation: false,
|
|
167
|
-
protocolVersion: "2024-01-01",
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
expect(isElicitationSupported(caps)).toBe(false);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("handles edge case protocol version exactly matching requirement", () => {
|
|
174
|
-
const caps: ClientCapabilities = {
|
|
175
|
-
...DEFAULT_CAPABILITIES,
|
|
176
|
-
elicitation: true,
|
|
177
|
-
protocolVersion: "2025-06-18",
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
expect(isElicitationSupported(caps)).toBe(true);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("handles protocol version one day before requirement", () => {
|
|
184
|
-
const caps: ClientCapabilities = {
|
|
185
|
-
...DEFAULT_CAPABILITIES,
|
|
186
|
-
elicitation: true,
|
|
187
|
-
protocolVersion: "2025-06-17",
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
expect(isElicitationSupported(caps)).toBe(false);
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
});
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
export interface ClientCapabilities {
|
|
2
|
-
sampling: boolean;
|
|
3
|
-
elicitation: boolean;
|
|
4
|
-
logging: boolean;
|
|
5
|
-
progress: boolean;
|
|
6
|
-
resourcesListChanged: boolean;
|
|
7
|
-
resourcesSubscribe: boolean;
|
|
8
|
-
protocolVersion: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const DEFAULT_CAPABILITIES: ClientCapabilities = {
|
|
12
|
-
sampling: false,
|
|
13
|
-
elicitation: false,
|
|
14
|
-
logging: false,
|
|
15
|
-
progress: false,
|
|
16
|
-
resourcesListChanged: false,
|
|
17
|
-
resourcesSubscribe: false,
|
|
18
|
-
protocolVersion: "unknown",
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export function hasSampling(caps: ClientCapabilities): boolean {
|
|
22
|
-
return caps.sampling;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function hasElicitation(caps: ClientCapabilities): boolean {
|
|
26
|
-
return caps.elicitation;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function hasLogging(caps: ClientCapabilities): boolean {
|
|
30
|
-
return caps.logging;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function hasProgress(caps: ClientCapabilities): boolean {
|
|
34
|
-
return caps.progress;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function hasResourcesListChanged(caps: ClientCapabilities): boolean {
|
|
38
|
-
return caps.resourcesListChanged;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function hasResourcesSubscribe(caps: ClientCapabilities): boolean {
|
|
42
|
-
return caps.resourcesSubscribe;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function isElicitationSupported(caps: ClientCapabilities): boolean {
|
|
46
|
-
// Elicitation requires protocol version 2025-06-18 or later
|
|
47
|
-
const version = caps.protocolVersion;
|
|
48
|
-
if (version === "unknown") return false;
|
|
49
|
-
return version >= "2025-06-18" && caps.elicitation;
|
|
50
|
-
}
|
package/src/types/context.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
-
import type { Config } from "../schemas/config.js";
|
|
3
|
-
import type { Logger } from "../logger.js";
|
|
4
|
-
import type { ClientCapabilities } from "./capabilities.js";
|
|
5
|
-
import type { PlaywrightClient } from "../playwright-client/index.js";
|
|
6
|
-
import type { CancellationRegistry } from "../progress/index.js";
|
|
7
|
-
import type { ResourceSubscriptions } from "../resources/subscriptions.js";
|
|
8
|
-
|
|
9
|
-
export interface ServerContext {
|
|
10
|
-
config: Config;
|
|
11
|
-
logger: Logger;
|
|
12
|
-
server: Server;
|
|
13
|
-
capabilities: ClientCapabilities;
|
|
14
|
-
playwrightClient: PlaywrightClient | null;
|
|
15
|
-
cancellationRegistry: CancellationRegistry;
|
|
16
|
-
resourceSubscriptions: ResourceSubscriptions;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type ServerContextWithPlaywright = ServerContext & {
|
|
20
|
-
playwrightClient: PlaywrightClient;
|
|
21
|
-
};
|
package/src/types/tool.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { z } from "zod";
|
|
2
|
-
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
-
|
|
4
|
-
export type ToolResult = CallToolResult;
|
|
5
|
-
|
|
6
|
-
export interface McpTool<TInput = unknown> {
|
|
7
|
-
name: string;
|
|
8
|
-
description: string;
|
|
9
|
-
inputSchema: z.ZodType<TInput>;
|
|
10
|
-
handler: (input: TInput) => Promise<ToolResult>;
|
|
11
|
-
}
|