@tehw0lf/contact-form 0.22.1 → 22.1.2

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.
@@ -0,0 +1,154 @@
1
+ const MAX_VALUE_LENGTH = 5000;
2
+ function describeControls(form) {
3
+ return Object.keys(form.controls);
4
+ }
5
+ function assertFields(fields, form) {
6
+ if (typeof fields !== 'object' || fields === null || Array.isArray(fields)) {
7
+ throw new Error('fields must be an object of control name to value');
8
+ }
9
+ const known = new Set(describeControls(form));
10
+ const result = {};
11
+ Object.entries(fields).forEach(([key, value]) => {
12
+ if (!known.has(key)) {
13
+ throw new Error(`unknown field "${key}", available fields: ${[...known].join(', ')}`);
14
+ }
15
+ if (typeof value !== 'string') {
16
+ throw new Error(`value for "${key}" must be a string`);
17
+ }
18
+ if (value.length > MAX_VALUE_LENGTH) {
19
+ throw new Error(`value for "${key}" exceeds ${MAX_VALUE_LENGTH} characters`);
20
+ }
21
+ result[key] = value;
22
+ });
23
+ return result;
24
+ }
25
+ /**
26
+ * Creates the contact form tools for a concrete rendered form.
27
+ *
28
+ * @param target the form group to describe and prefill
29
+ */
30
+ function createContactFormTools(target) {
31
+ const describeTool = {
32
+ name: 'contact_form_describe',
33
+ title: 'Describe the contact form',
34
+ description: 'Lists the fields of the contact form on this page, whether they are currently valid and what is already filled in.',
35
+ inputSchema: { type: 'object', properties: {} },
36
+ annotations: { readOnlyHint: true },
37
+ execute: async () => {
38
+ const { form, requiredFields } = target;
39
+ const required = new Set(requiredFields ?? []);
40
+ return {
41
+ fields: describeControls(form).map((name) => {
42
+ const control = form.get(name);
43
+ return {
44
+ name,
45
+ filled: Boolean(control?.value),
46
+ valid: control?.valid ?? false,
47
+ required: required.has(name)
48
+ };
49
+ }),
50
+ formValid: form.valid
51
+ };
52
+ }
53
+ };
54
+ const prefillTool = {
55
+ name: 'contact_form_prefill',
56
+ title: 'Prefill the contact form',
57
+ description: 'Fills fields of the contact form on this page so the user can review them. This never sends the message; the user has to press send themselves.',
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: {
61
+ fields: {
62
+ type: 'object',
63
+ description: 'Map of field name to value. Call contact_form_describe first to learn the available field names.',
64
+ additionalProperties: { type: 'string' }
65
+ }
66
+ },
67
+ required: ['fields']
68
+ },
69
+ // Not read-only: it changes what the user sees in the form.
70
+ annotations: { readOnlyHint: false },
71
+ execute: async ({ fields }) => {
72
+ const { form } = target;
73
+ const validated = assertFields(fields, form);
74
+ form.patchValue(validated);
75
+ form.markAsDirty();
76
+ return {
77
+ prefilled: Object.keys(validated),
78
+ formValid: form.valid,
79
+ submitted: false,
80
+ note: 'The form was filled in only. The user still has to press send.'
81
+ };
82
+ }
83
+ };
84
+ return [
85
+ describeTool,
86
+ prefillTool
87
+ ];
88
+ }
89
+
90
+ /**
91
+ * Minimal typing and access layer for the WebMCP browser API.
92
+ *
93
+ * WebMCP is a W3C Community Group draft (not on the standards track) and the
94
+ * global it lives on has already moved twice: `window.agent`, then
95
+ * `navigator.modelContext`, and since the 21 July 2026 draft
96
+ * `document.modelContext`. Chrome 150 deprecated the navigator location but
97
+ * still exposes it as an alias.
98
+ *
99
+ * Every access goes through `getModelContext()` so a further move only has to
100
+ * be handled here instead of in each tool.
101
+ */
102
+ /**
103
+ * Returns the ModelContext of the current document, or undefined when the
104
+ * browser does not implement WebMCP. Prefers the current `document` location
105
+ * and falls back to the deprecated `navigator` alias.
106
+ */
107
+ function getModelContext() {
108
+ if (typeof document === 'undefined') {
109
+ return undefined;
110
+ }
111
+ const fromDocument = document
112
+ .modelContext;
113
+ if (fromDocument) {
114
+ return fromDocument;
115
+ }
116
+ if (typeof navigator === 'undefined') {
117
+ return undefined;
118
+ }
119
+ return navigator.modelContext;
120
+ }
121
+ /** True when this browser exposes the WebMCP API. */
122
+ function isModelContextSupported() {
123
+ return getModelContext() !== undefined;
124
+ }
125
+ /**
126
+ * Registers tools if the browser supports WebMCP, otherwise resolves without
127
+ * doing anything. Returns a function that unregisters them again.
128
+ *
129
+ * Registration mutates global browser state, so libraries must never call this
130
+ * on their own; it is opt-in for the embedding application.
131
+ */
132
+ async function registerModelContextTools(tools) {
133
+ const modelContext = getModelContext();
134
+ if (!modelContext) {
135
+ return () => undefined;
136
+ }
137
+ const controller = new AbortController();
138
+ await Promise.all(tools.map((tool) => modelContext
139
+ .registerTool(tool, { signal: controller.signal })
140
+ .catch((error) => {
141
+ // Tool names come from the caller, so they are passed as arguments
142
+ // rather than interpolated: a name containing a format specifier
143
+ // would otherwise consume the error and forge the log line.
144
+ console.warn('WebMCP: registering tool failed:', tool.name, error);
145
+ })));
146
+ return () => controller.abort();
147
+ }
148
+
149
+ /**
150
+ * Generated bundle index. Do not edit.
151
+ */
152
+
153
+ export { createContactFormTools, getModelContext, isModelContextSupported, registerModelContextTools };
154
+ //# sourceMappingURL=tehw0lf-contact-form-webmcp.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tehw0lf-contact-form-webmcp.mjs","sources":["../../../../libs/contact-form/src/lib/webmcp/contact-form-tools.ts","../../../../libs/contact-form/src/lib/webmcp/model-context.ts","../../../../libs/contact-form/src/lib/webmcp/tehw0lf-contact-form-webmcp.ts"],"sourcesContent":["import { FormGroup } from '@angular/forms';\n\nimport { ModelContextTool } from './model-context';\n\n/**\n * A tool that could submit the form would let an agent send mail on a\n * visitor's behalf, which is a spam vector. These tools therefore only ever\n * fill fields in and report the state back; pressing send stays a human\n * action.\n */\nexport interface ContactFormToolTarget {\n /** The form group rendered by ContactFormComponent. */\n form: FormGroup;\n /**\n * The component's formConfig, which is what actually declares whether a\n * field is required.\n *\n * The control state cannot answer this: ngx-formly expresses required as a\n * validator expression rather than Validators.required, so hasValidator()\n * misses it, and errors['required'] disappears as soon as the field is\n * filled. Fields absent from the config are reported as not required.\n */\n requiredFields?: readonly string[];\n}\n\nconst MAX_VALUE_LENGTH = 5000;\n\nfunction describeControls(form: FormGroup): string[] {\n return Object.keys(form.controls);\n}\n\nfunction assertFields(\n fields: unknown,\n form: FormGroup\n): Record<string, string> {\n if (typeof fields !== 'object' || fields === null || Array.isArray(fields)) {\n throw new Error('fields must be an object of control name to value');\n }\n\n const known = new Set(describeControls(form));\n const result: Record<string, string> = {};\n\n Object.entries(fields as Record<string, unknown>).forEach(([key, value]) => {\n if (!known.has(key)) {\n throw new Error(\n `unknown field \"${key}\", available fields: ${[...known].join(', ')}`\n );\n }\n if (typeof value !== 'string') {\n throw new Error(`value for \"${key}\" must be a string`);\n }\n if (value.length > MAX_VALUE_LENGTH) {\n throw new Error(\n `value for \"${key}\" exceeds ${MAX_VALUE_LENGTH} characters`\n );\n }\n result[key] = value;\n });\n\n return result;\n}\n\n/**\n * Creates the contact form tools for a concrete rendered form.\n *\n * @param target the form group to describe and prefill\n */\nexport function createContactFormTools(\n target: ContactFormToolTarget\n): readonly ModelContextTool<never>[] {\n const describeTool: ModelContextTool<Record<string, never>> = {\n name: 'contact_form_describe',\n title: 'Describe the contact form',\n description:\n 'Lists the fields of the contact form on this page, whether they are currently valid and what is already filled in.',\n inputSchema: { type: 'object', properties: {} },\n annotations: { readOnlyHint: true },\n execute: async () => {\n const { form, requiredFields } = target;\n const required = new Set(requiredFields ?? []);\n\n return {\n fields: describeControls(form).map((name) => {\n const control = form.get(name);\n return {\n name,\n filled: Boolean(control?.value),\n valid: control?.valid ?? false,\n required: required.has(name)\n };\n }),\n formValid: form.valid\n };\n }\n };\n\n const prefillTool: ModelContextTool<{ fields: Record<string, string> }> = {\n name: 'contact_form_prefill',\n title: 'Prefill the contact form',\n description:\n 'Fills fields of the contact form on this page so the user can review them. This never sends the message; the user has to press send themselves.',\n inputSchema: {\n type: 'object',\n properties: {\n fields: {\n type: 'object',\n description:\n 'Map of field name to value. Call contact_form_describe first to learn the available field names.',\n additionalProperties: { type: 'string' }\n }\n },\n required: ['fields']\n },\n // Not read-only: it changes what the user sees in the form.\n annotations: { readOnlyHint: false },\n execute: async ({ fields }) => {\n const { form } = target;\n const validated = assertFields(fields, form);\n\n form.patchValue(validated);\n form.markAsDirty();\n\n return {\n prefilled: Object.keys(validated),\n formValid: form.valid,\n submitted: false,\n note: 'The form was filled in only. The user still has to press send.'\n };\n }\n };\n\n return [\n describeTool as unknown as ModelContextTool<never>,\n prefillTool as unknown as ModelContextTool<never>\n ];\n}\n","/**\n * Minimal typing and access layer for the WebMCP browser API.\n *\n * WebMCP is a W3C Community Group draft (not on the standards track) and the\n * global it lives on has already moved twice: `window.agent`, then\n * `navigator.modelContext`, and since the 21 July 2026 draft\n * `document.modelContext`. Chrome 150 deprecated the navigator location but\n * still exposes it as an alias.\n *\n * Every access goes through `getModelContext()` so a further move only has to\n * be handled here instead of in each tool.\n */\n\n/** Result an agent receives back from a tool call. */\nexport type ModelContextToolResult = string | Record<string, unknown>;\n\nexport interface ModelContextToolAnnotations {\n /** The tool only reads state and is safe to call speculatively. */\n readOnlyHint?: boolean;\n /** The tool returns content that originates from outside this page. */\n untrustedContentHint?: boolean;\n}\n\nexport interface ModelContextTool<TInput = Record<string, unknown>> {\n name: string;\n title?: string;\n description: string;\n inputSchema?: Record<string, unknown>;\n annotations?: ModelContextToolAnnotations;\n execute: (input: TInput) => Promise<ModelContextToolResult>;\n}\n\nexport interface ModelContextRegisterOptions {\n /** Aborting the signal unregisters the tool again. */\n signal?: AbortSignal;\n}\n\nexport interface ModelContext {\n registerTool(\n tool: ModelContextTool<never>,\n options?: ModelContextRegisterOptions\n ): Promise<void>;\n}\n\ninterface ModelContextCarrier {\n modelContext?: ModelContext;\n}\n\n/**\n * Returns the ModelContext of the current document, or undefined when the\n * browser does not implement WebMCP. Prefers the current `document` location\n * and falls back to the deprecated `navigator` alias.\n */\nexport function getModelContext(): ModelContext | undefined {\n if (typeof document === 'undefined') {\n return undefined;\n }\n\n const fromDocument = (document as unknown as ModelContextCarrier)\n .modelContext;\n if (fromDocument) {\n return fromDocument;\n }\n\n if (typeof navigator === 'undefined') {\n return undefined;\n }\n\n return (navigator as unknown as ModelContextCarrier).modelContext;\n}\n\n/** True when this browser exposes the WebMCP API. */\nexport function isModelContextSupported(): boolean {\n return getModelContext() !== undefined;\n}\n\n/**\n * Registers tools if the browser supports WebMCP, otherwise resolves without\n * doing anything. Returns a function that unregisters them again.\n *\n * Registration mutates global browser state, so libraries must never call this\n * on their own; it is opt-in for the embedding application.\n */\nexport async function registerModelContextTools(\n tools: readonly ModelContextTool<never>[]\n): Promise<() => void> {\n const modelContext = getModelContext();\n if (!modelContext) {\n return () => undefined;\n }\n\n const controller = new AbortController();\n\n await Promise.all(\n tools.map((tool) =>\n modelContext\n .registerTool(tool, { signal: controller.signal })\n .catch((error: unknown) => {\n // Tool names come from the caller, so they are passed as arguments\n // rather than interpolated: a name containing a format specifier\n // would otherwise consume the error and forge the log line.\n console.warn('WebMCP: registering tool failed:', tool.name, error);\n })\n )\n );\n\n return () => controller.abort();\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -121,10 +121,10 @@ class ContactFormComponent {
121
121
  })
122
122
  .join('');
123
123
  }
124
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ContactFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
125
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ContactFormComponent, isStandalone: true, selector: "contact-form", inputs: { buttonStyle: { classPropertyName: "buttonStyle", publicName: "buttonStyle", isSignal: true, isRequired: false, transformFunction: null }, formStyle: { classPropertyName: "formStyle", publicName: "formStyle", isSignal: true, isRequired: false, transformFunction: null }, textStyle: { classPropertyName: "textStyle", publicName: "textStyle", isSignal: true, isRequired: false, transformFunction: null }, sendText: { classPropertyName: "sendText", publicName: "sendText", isSignal: true, isRequired: false, transformFunction: null }, sendSuccessfulText: { classPropertyName: "sendSuccessfulText", publicName: "sendSuccessfulText", isSignal: true, isRequired: false, transformFunction: null }, sendErrorText: { classPropertyName: "sendErrorText", publicName: "sendErrorText", isSignal: true, isRequired: false, transformFunction: null }, formConfig: { classPropertyName: "formConfig", publicName: "formConfig", isSignal: true, isRequired: false, transformFunction: null }, apiCallback: { classPropertyName: "apiCallback", publicName: "apiCallback", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<form\n [ngStyle]=\"formStyle()\"\n class=\"contact-form flex-column flex-fxflex\"\n [formGroup]=\"form\"\n (ngSubmit)=\"submitFormData(model)\"\n>\n <formly-form [form]=\"form\" [fields]=\"fields\" [model]=\"model\"></formly-form>\n <button\n [ngStyle]=\"buttonStyle()\"\n class=\"form-button\"\n type=\"submit\"\n [disabled]=\"!form.valid\"\n >\n {{ sendText() }}\n </button>\n <div [ngStyle]=\"textStyle()\" class=\"form-status\">\n @if (emailSent()) {\n {{ sendSuccessfulText() }}\n } @else if (emailSent() === false) {\n {{ sendErrorText() }}\n }\n </div>\n</form>\n", styles: [".flex-align-start{justify-content:flex-start}.flex-align-stretch{align-content:stretch;align-items:stretch}.flex-column{display:flex;flex-direction:column;box-sizing:border-box}.flex-row{display:flex;flex-direction:row;box-sizing:border-box}.flex-row-wrap{display:flex;flex-direction:row;flex-wrap:wrap;box-sizing:border-box}.flex-fxflex{flex:1 1 0%}.flex-fxflex-responsive{flex:0 1 calc(33.3% - 32px)}.flex-fxflex-lt-md{flex:0 1 calc(50% - 32px)}.flex-fxflex-lt-sm{flex:100%}.flex-fxflex-fill{height:100%;min-height:100%;min-width:100%;width:100%}.flex-gap-5{margin-right:5px}.flex-gap-12{margin-right:12px}.flex-gap-20{margin-right:20px}.contact-form{max-width:500px;margin:auto;padding:40px 40px 30px;box-sizing:border-box}.form-button{margin-top:.33rem;display:block;border-radius:.25rem;height:36px;width:100%;padding:.375rem .75rem}.form-status{height:19px;margin-top:1rem}\n"], dependencies: [{ kind: "ngmodule", type: LayoutModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FormlyModule }, { kind: "component", type: i2.LegacyFormlyForm, selector: "formly-form" }, { kind: "ngmodule", type: FormlyMaterialModule }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
124
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ContactFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
125
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: ContactFormComponent, isStandalone: true, selector: "contact-form", inputs: { buttonStyle: { classPropertyName: "buttonStyle", publicName: "buttonStyle", isSignal: true, isRequired: false, transformFunction: null }, formStyle: { classPropertyName: "formStyle", publicName: "formStyle", isSignal: true, isRequired: false, transformFunction: null }, textStyle: { classPropertyName: "textStyle", publicName: "textStyle", isSignal: true, isRequired: false, transformFunction: null }, sendText: { classPropertyName: "sendText", publicName: "sendText", isSignal: true, isRequired: false, transformFunction: null }, sendSuccessfulText: { classPropertyName: "sendSuccessfulText", publicName: "sendSuccessfulText", isSignal: true, isRequired: false, transformFunction: null }, sendErrorText: { classPropertyName: "sendErrorText", publicName: "sendErrorText", isSignal: true, isRequired: false, transformFunction: null }, formConfig: { classPropertyName: "formConfig", publicName: "formConfig", isSignal: true, isRequired: false, transformFunction: null }, apiCallback: { classPropertyName: "apiCallback", publicName: "apiCallback", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<form\n [ngStyle]=\"formStyle()\"\n class=\"contact-form flex-column flex-fxflex\"\n [formGroup]=\"form\"\n (ngSubmit)=\"submitFormData(model)\"\n>\n <formly-form [form]=\"form\" [fields]=\"fields\" [model]=\"model\"></formly-form>\n <button\n [ngStyle]=\"buttonStyle()\"\n class=\"form-button\"\n type=\"submit\"\n [disabled]=\"!form.valid\"\n >\n {{ sendText() }}\n </button>\n <div [ngStyle]=\"textStyle()\" class=\"form-status\">\n @if (emailSent()) {\n {{ sendSuccessfulText() }}\n } @else if (emailSent() === false) {\n {{ sendErrorText() }}\n }\n </div>\n</form>\n", styles: [".flex-align-start{justify-content:flex-start}.flex-align-stretch{align-content:stretch;align-items:stretch}.flex-column{display:flex;flex-direction:column;box-sizing:border-box}.flex-row{display:flex;flex-direction:row;box-sizing:border-box}.flex-row-wrap{display:flex;flex-direction:row;flex-wrap:wrap;box-sizing:border-box}.flex-fxflex{flex:1 1 0%}.flex-fxflex-responsive{flex:0 1 calc(33.3% - 32px)}.flex-fxflex-lt-md{flex:0 1 calc(50% - 32px)}.flex-fxflex-lt-sm{flex:100%}.flex-fxflex-fill{height:100%;min-height:100%;min-width:100%;width:100%}.flex-gap-5{margin-right:5px}.flex-gap-12{margin-right:12px}.flex-gap-20{margin-right:20px}.contact-form{max-width:500px;margin:auto;padding:40px 40px 30px;box-sizing:border-box}.form-button{margin-top:.33rem;display:block;border-radius:.25rem;height:36px;width:100%;padding:.375rem .75rem}.form-status{height:19px;margin-top:1rem}\n"], dependencies: [{ kind: "ngmodule", type: LayoutModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FormlyModule }, { kind: "component", type: i2.LegacyFormlyForm, selector: "formly-form" }, { kind: "ngmodule", type: FormlyMaterialModule }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
126
126
  }
127
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ContactFormComponent, decorators: [{
127
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ContactFormComponent, decorators: [{
128
128
  type: Component,
129
129
  args: [{ selector: 'contact-form', encapsulation: ViewEncapsulation.None, imports: [
130
130
  LayoutModule,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tehw0lf/contact-form",
3
- "version": "0.22.1",
3
+ "version": "22.1.2",
4
4
  "license": "MIT",
5
5
  "homepage": "https://github.com/tehw0lf/tehwol.fi.git",
6
6
  "repository": {
@@ -8,9 +8,9 @@
8
8
  "url": "git+https://github.com/tehw0lf/tehwol.fi.git"
9
9
  },
10
10
  "peerDependencies": {
11
- "@angular/common": "22.0.2",
12
- "@angular/core": "22.0.2",
13
- "@angular/forms": "22.0.2",
11
+ "@angular/common": "22.0.8",
12
+ "@angular/core": "22.0.8",
13
+ "@angular/forms": "22.0.8",
14
14
  "@ngx-formly/core": "7.1.0",
15
15
  "@ngx-formly/material": "7.1.0",
16
16
  "@tehw0lf/mvc": "0.0.8"
@@ -27,6 +27,10 @@
27
27
  ".": {
28
28
  "types": "./types/tehw0lf-contact-form.d.ts",
29
29
  "default": "./fesm2022/tehw0lf-contact-form.mjs"
30
+ },
31
+ "./webmcp": {
32
+ "types": "./types/tehw0lf-contact-form-webmcp.d.ts",
33
+ "default": "./fesm2022/tehw0lf-contact-form-webmcp.mjs"
30
34
  }
31
35
  },
32
36
  "sideEffects": false,
@@ -0,0 +1,83 @@
1
+ import { FormGroup } from '@angular/forms';
2
+
3
+ /**
4
+ * Minimal typing and access layer for the WebMCP browser API.
5
+ *
6
+ * WebMCP is a W3C Community Group draft (not on the standards track) and the
7
+ * global it lives on has already moved twice: `window.agent`, then
8
+ * `navigator.modelContext`, and since the 21 July 2026 draft
9
+ * `document.modelContext`. Chrome 150 deprecated the navigator location but
10
+ * still exposes it as an alias.
11
+ *
12
+ * Every access goes through `getModelContext()` so a further move only has to
13
+ * be handled here instead of in each tool.
14
+ */
15
+ /** Result an agent receives back from a tool call. */
16
+ type ModelContextToolResult = string | Record<string, unknown>;
17
+ interface ModelContextToolAnnotations {
18
+ /** The tool only reads state and is safe to call speculatively. */
19
+ readOnlyHint?: boolean;
20
+ /** The tool returns content that originates from outside this page. */
21
+ untrustedContentHint?: boolean;
22
+ }
23
+ interface ModelContextTool<TInput = Record<string, unknown>> {
24
+ name: string;
25
+ title?: string;
26
+ description: string;
27
+ inputSchema?: Record<string, unknown>;
28
+ annotations?: ModelContextToolAnnotations;
29
+ execute: (input: TInput) => Promise<ModelContextToolResult>;
30
+ }
31
+ interface ModelContextRegisterOptions {
32
+ /** Aborting the signal unregisters the tool again. */
33
+ signal?: AbortSignal;
34
+ }
35
+ interface ModelContext {
36
+ registerTool(tool: ModelContextTool<never>, options?: ModelContextRegisterOptions): Promise<void>;
37
+ }
38
+ /**
39
+ * Returns the ModelContext of the current document, or undefined when the
40
+ * browser does not implement WebMCP. Prefers the current `document` location
41
+ * and falls back to the deprecated `navigator` alias.
42
+ */
43
+ declare function getModelContext(): ModelContext | undefined;
44
+ /** True when this browser exposes the WebMCP API. */
45
+ declare function isModelContextSupported(): boolean;
46
+ /**
47
+ * Registers tools if the browser supports WebMCP, otherwise resolves without
48
+ * doing anything. Returns a function that unregisters them again.
49
+ *
50
+ * Registration mutates global browser state, so libraries must never call this
51
+ * on their own; it is opt-in for the embedding application.
52
+ */
53
+ declare function registerModelContextTools(tools: readonly ModelContextTool<never>[]): Promise<() => void>;
54
+
55
+ /**
56
+ * A tool that could submit the form would let an agent send mail on a
57
+ * visitor's behalf, which is a spam vector. These tools therefore only ever
58
+ * fill fields in and report the state back; pressing send stays a human
59
+ * action.
60
+ */
61
+ interface ContactFormToolTarget {
62
+ /** The form group rendered by ContactFormComponent. */
63
+ form: FormGroup;
64
+ /**
65
+ * The component's formConfig, which is what actually declares whether a
66
+ * field is required.
67
+ *
68
+ * The control state cannot answer this: ngx-formly expresses required as a
69
+ * validator expression rather than Validators.required, so hasValidator()
70
+ * misses it, and errors['required'] disappears as soon as the field is
71
+ * filled. Fields absent from the config are reported as not required.
72
+ */
73
+ requiredFields?: readonly string[];
74
+ }
75
+ /**
76
+ * Creates the contact form tools for a concrete rendered form.
77
+ *
78
+ * @param target the form group to describe and prefill
79
+ */
80
+ declare function createContactFormTools(target: ContactFormToolTarget): readonly ModelContextTool<never>[];
81
+
82
+ export { createContactFormTools, getModelContext, isModelContextSupported, registerModelContextTools };
83
+ export type { ContactFormToolTarget, ModelContext, ModelContextRegisterOptions, ModelContextTool, ModelContextToolAnnotations, ModelContextToolResult };
@@ -0,0 +1,4 @@
1
+ {
2
+ "module": "../fesm2022/tehw0lf-contact-form-webmcp.mjs",
3
+ "typings": "../types/tehw0lf-contact-form-webmcp.d.ts"
4
+ }