@nxisai/nxis 2.1.2 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy.yml +45 -7
- package/README.md +185 -1
- package/build/api.d.ts +13 -2
- package/build/core.d.ts +1 -1
- package/build/nxis.js +1 -1
- package/build/webmcp/contract.d.ts +109 -0
- package/build/webmcp/execute.d.ts +24 -0
- package/build/webmcp/index.d.ts +37 -0
- package/build/webmcp/locate.d.ts +19 -0
- package/build/webmcp/parse.d.ts +20 -0
- package/build/webmcp/reconcile.d.ts +57 -0
- package/build/webmcp/register.d.ts +31 -0
- package/build/webmcp/schema.d.ts +30 -0
- package/build/webmcp/types.d.ts +72 -0
- package/build/webmcp.js +1 -0
- package/package.json +10 -2
- package/rollup.config.js +20 -1
- package/src/api.ts +13 -2
- package/src/client.ts +23 -13
- package/src/core.ts +1 -2
- package/src/webmcp/contract.ts +168 -0
- package/src/webmcp/execute.ts +514 -0
- package/src/webmcp/index.ts +51 -0
- package/src/webmcp/locate.ts +200 -0
- package/src/webmcp/parse.ts +276 -0
- package/src/webmcp/reconcile.ts +170 -0
- package/src/webmcp/register.ts +285 -0
- package/src/webmcp/schema.ts +201 -0
- package/src/webmcp/types.ts +86 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM reconciliation — cross-checks the Action's claimed descriptors
|
|
3
|
+
* against the resolved form's real controls.
|
|
4
|
+
*
|
|
5
|
+
* The live DOM is authoritative. Where the JSON-LD and the form's
|
|
6
|
+
* real controls conflict, we trust the DOM.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
import { NxisAction, NxisPropertyValueSpecification } from './contract';
|
|
11
|
+
/** Fill strategy for a single reconciled field. */
|
|
12
|
+
export type FillStrategy = {
|
|
13
|
+
kind: 'value';
|
|
14
|
+
element: HTMLInputElement | HTMLTextAreaElement;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'checked';
|
|
17
|
+
element: HTMLInputElement;
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'radio';
|
|
20
|
+
name: string;
|
|
21
|
+
elements: HTMLInputElement[];
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'checkboxGroup';
|
|
24
|
+
name: string;
|
|
25
|
+
elements: HTMLInputElement[];
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'select';
|
|
28
|
+
element: HTMLSelectElement;
|
|
29
|
+
allowedValues: string[];
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'multiSelect';
|
|
32
|
+
element: HTMLSelectElement;
|
|
33
|
+
allowedValues: string[];
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'skip';
|
|
36
|
+
reason: string;
|
|
37
|
+
};
|
|
38
|
+
/** A reconciled field — pairs a valueName to its real element + fill strategy. */
|
|
39
|
+
export interface ReconciledField {
|
|
40
|
+
valueName: string;
|
|
41
|
+
pvs: NxisPropertyValueSpecification;
|
|
42
|
+
strategy: FillStrategy;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Reconcile the Action's claimed field descriptors against the form's real controls.
|
|
46
|
+
*
|
|
47
|
+
* Rules (from implementation plan):
|
|
48
|
+
* - valueName with no matching control → skip (don't fail the whole fill)
|
|
49
|
+
* - Real control type contradicts claimed valueType → follow the DOM
|
|
50
|
+
* - Real control is <select> → constrain to its actual <option>s
|
|
51
|
+
* - readonlyValue mismatch → trust DOM readonly attribute
|
|
52
|
+
*
|
|
53
|
+
* @param action - The parsed Action with claimed descriptors.
|
|
54
|
+
* @param form - The located form element.
|
|
55
|
+
* @returns Array of ReconciledField objects.
|
|
56
|
+
*/
|
|
57
|
+
export declare function reconcileFields(action: NxisAction, form: HTMLFormElement): ReconciledField[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registration — registers NxisActions as WebMCP tools.
|
|
3
|
+
*
|
|
4
|
+
* Ensures WebMCP availability via @nxisai/webmcp polyfill (no-ops when
|
|
5
|
+
* a native implementation exists, e.g. Chrome 149+).
|
|
6
|
+
*
|
|
7
|
+
* Uses `document.modelContext` (canonical, Chrome 150+) with
|
|
8
|
+
* `navigator.modelContext` fallback for older builds.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
import './types';
|
|
13
|
+
export interface RegisterOptions {
|
|
14
|
+
/** Document to scan for JSON-LD blocks. Defaults to `document`. */
|
|
15
|
+
doc?: Document;
|
|
16
|
+
/** AbortSignal to externally cancel all registrations. */
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
/** Enable MutationObserver re-scan for SPAs. Defaults to `true` (opt-out). */
|
|
19
|
+
observe?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse all Action JSON-LD from the page and register each as a WebMCP tool.
|
|
23
|
+
*
|
|
24
|
+
* Automatically installs the @nxisai/webmcp polyfill on browsers without
|
|
25
|
+
* native WebMCP support. The polyfill is a no-op when `document.modelContext`
|
|
26
|
+
* already exists (Chrome 149+).
|
|
27
|
+
*
|
|
28
|
+
* @param options - Configuration options.
|
|
29
|
+
* @returns A cleanup function that unregisters all tools and disconnects observers.
|
|
30
|
+
*/
|
|
31
|
+
export declare function registerActions(options?: RegisterOptions): () => void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema transform — converts NxisAction PVS descriptors to JSON Schema
|
|
3
|
+
* for the WebMCP `registerTool` inputSchema.
|
|
4
|
+
*
|
|
5
|
+
* Pure function, no DOM access.
|
|
6
|
+
*
|
|
7
|
+
* Mapping rules (from implementation plan):
|
|
8
|
+
* - valueName → property key
|
|
9
|
+
* - nxis:valueType → JSON Schema "type" (date/file → "string")
|
|
10
|
+
* - valueRequired → sibling "required" array (not per-property keyword)
|
|
11
|
+
* - valuePattern → anchored with ^…$ (emitted unanchored by producer)
|
|
12
|
+
* - multipleValues → nests into { type: "array", items: { ... } }
|
|
13
|
+
* - nxis:valueOption → "enum" or "oneOf" with { const, title }
|
|
14
|
+
* - defaultValue → "default"
|
|
15
|
+
* - description → "description"
|
|
16
|
+
* - valueMinLength/valueMaxLength → minLength/maxLength
|
|
17
|
+
* - minValue/maxValue → minimum/maximum
|
|
18
|
+
* - stepValue → multipleOf
|
|
19
|
+
* - readonlyValue → readOnly: true
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
import { NxisAction } from './contract';
|
|
24
|
+
/**
|
|
25
|
+
* Build a JSON Schema object for the Action's input parameters.
|
|
26
|
+
*
|
|
27
|
+
* @param action - The parsed Action to transform.
|
|
28
|
+
* @returns A JSON Schema `{ type: "object", properties: { ... }, required: [...] }`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildInputSchema(action: NxisAction): Record<string, unknown>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for the WebMCP imperative API.
|
|
3
|
+
*
|
|
4
|
+
* Augments the global `Document` and `Navigator` interfaces with
|
|
5
|
+
* `modelContext` so TypeScript recognizes the WebMCP surface.
|
|
6
|
+
*
|
|
7
|
+
* `document.modelContext` is canonical (Chrome 150+).
|
|
8
|
+
* `navigator.modelContext` is deprecated but supported as fallback.
|
|
9
|
+
*
|
|
10
|
+
* @see https://developer.chrome.com/docs/ai/webmcp/imperative-api
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
/** Annotations that hint to the agent how to treat a tool. */
|
|
14
|
+
export interface ToolAnnotations {
|
|
15
|
+
/**
|
|
16
|
+
* If true, the tool only reads data and does not mutate the page.
|
|
17
|
+
* Set to `false` for form-filling tools (we mutate input values).
|
|
18
|
+
*/
|
|
19
|
+
readOnlyHint?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* If true, the tool's output contains untrusted content from the page.
|
|
22
|
+
* Always `true` for NxisAI tools — we infer from content we don't control.
|
|
23
|
+
*/
|
|
24
|
+
untrustedContentHint?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** The shape passed to `modelContext.registerTool()`. */
|
|
27
|
+
export interface ModelContextTool {
|
|
28
|
+
/** Tool name — unique within the document. */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Human-readable description of what the tool does. */
|
|
31
|
+
description: string;
|
|
32
|
+
/**
|
|
33
|
+
* JSON Schema describing the tool's input parameters.
|
|
34
|
+
* Passed as a plain object (type: "object" with properties).
|
|
35
|
+
*/
|
|
36
|
+
inputSchema: Record<string, unknown>;
|
|
37
|
+
/** Behavioral hints for the agent. */
|
|
38
|
+
annotations?: ToolAnnotations;
|
|
39
|
+
/**
|
|
40
|
+
* Called by the agent to execute the tool.
|
|
41
|
+
* Must return a string (JSON-serialized result).
|
|
42
|
+
*/
|
|
43
|
+
execute: (args: Record<string, unknown>) => string | Promise<string>;
|
|
44
|
+
}
|
|
45
|
+
/** Options for `modelContext.registerTool()`. */
|
|
46
|
+
export interface RegisterToolOptions {
|
|
47
|
+
/** AbortSignal to unregister the tool when aborted. */
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}
|
|
50
|
+
/** The ModelContext interface exposed on `document.modelContext`. */
|
|
51
|
+
export interface ModelContext extends EventTarget {
|
|
52
|
+
/** Register a tool for agents to discover and invoke. */
|
|
53
|
+
registerTool(tool: ModelContextTool, options?: RegisterToolOptions): void;
|
|
54
|
+
/** Get all currently registered tools. */
|
|
55
|
+
getTools(): ModelContextTool[];
|
|
56
|
+
}
|
|
57
|
+
declare global {
|
|
58
|
+
interface Document {
|
|
59
|
+
/**
|
|
60
|
+
* The WebMCP model context (canonical, Chrome 150+).
|
|
61
|
+
* Tools are per-Document, not per-Navigator.
|
|
62
|
+
*/
|
|
63
|
+
modelContext?: ModelContext;
|
|
64
|
+
}
|
|
65
|
+
interface Navigator {
|
|
66
|
+
/**
|
|
67
|
+
* @deprecated Use `document.modelContext` instead.
|
|
68
|
+
* Deprecated as of Chrome 150.0.7861.0.
|
|
69
|
+
*/
|
|
70
|
+
modelContext?: ModelContext;
|
|
71
|
+
}
|
|
72
|
+
}
|
package/build/webmcp.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){return null===e?"null":Array.isArray(e)?"array":typeof e}function t(n,o){if(n===o)return!0;if(e(n)!==e(o))return!1;if(Array.isArray(n)&&Array.isArray(o))return n.length===o.length&&n.every((e,n)=>t(e,o[n]));if("object"===e(n)){const e=n,r=o,i=Object.keys(e);return i.length===Object.keys(r).length&&i.every(n=>t(e[n],r[n]))}return!1}function n(o,r,i="$"){if(!r||"object"!=typeof r)return[];const a=[];if(void 0===r.const||t(o,r.const)||a.push({path:i,message:`must equal const ${JSON.stringify(r.const)}`}),r.enum&&!r.enum.some(e=>t(o,e))&&a.push({path:i,message:`must be one of ${JSON.stringify(r.enum)}`}),void 0!==r.type){const t=Array.isArray(r.type)?r.type:[r.type];if(!t.some(t=>function(t,n){switch(n){case"object":return"object"===e(t);case"array":return Array.isArray(t);case"string":return"string"==typeof t;case"number":return"number"==typeof t&&Number.isFinite(t);case"integer":return"number"==typeof t&&Number.isInteger(t);case"boolean":return"boolean"==typeof t;case"null":return null===t;default:return!0}}(o,t)))return a.push({path:i,message:`expected type ${t.join(" | ")}, got ${e(o)}`}),a}if("string"==typeof o&&(void 0!==r.minLength&&o.length<r.minLength&&a.push({path:i,message:`must be at least ${r.minLength} characters`}),void 0!==r.maxLength&&o.length>r.maxLength&&a.push({path:i,message:`must be at most ${r.maxLength} characters`}),void 0!==r.pattern)){let e=null;try{e=new RegExp(r.pattern,"u")}catch{}e&&!e.test(o)&&a.push({path:i,message:`must match pattern ${r.pattern}`})}if("number"==typeof o&&(void 0!==r.minimum&&o<r.minimum&&a.push({path:i,message:`must be >= ${r.minimum}`}),void 0!==r.maximum&&o>r.maximum&&a.push({path:i,message:`must be <= ${r.maximum}`})),Array.isArray(o)&&(void 0!==r.minItems&&o.length<r.minItems&&a.push({path:i,message:`must have at least ${r.minItems} items`}),void 0!==r.maxItems&&o.length>r.maxItems&&a.push({path:i,message:`must have at most ${r.maxItems} items`}),r.items&&o.forEach((e,t)=>{a.push(...n(e,r.items,`${i}[${t}]`))})),"object"===e(o)&&!Array.isArray(o)&&null!==o){const e=o;for(const t of r.required??[])t in e||a.push({path:i,message:`missing required property "${t}"`});const t=r.properties??{};for(const[o,r]of Object.entries(t))o in e&&a.push(...n(e[o],r,`${i}.${o}`));if(!1===r.additionalProperties)for(const n of Object.keys(e))n in t||a.push({path:i,message:`unexpected property "${n}"`});else if(r.additionalProperties&&"object"==typeof r.additionalProperties)for(const o of Object.keys(e))o in t||a.push(...n(e[o],r.additionalProperties,`${i}.${o}`))}if(r.allOf)for(const e of r.allOf)a.push(...n(o,e,i));if(r.anyOf&&r.anyOf.length>0){r.anyOf.some(e=>0===n(o,e,i).length)||a.push({path:i,message:"does not match any schema in anyOf"})}if(r.oneOf&&r.oneOf.length>0){const e=r.oneOf.filter(e=>0===n(o,e,i).length).length;1!==e&&a.push({path:i,message:`must match exactly one schema in oneOf (matched ${e})`})}return a}var o,r,i,a,s,l,u=function(e,t,n,o){if("a"===n&&!o)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!o:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?o:"a"===n?o.call(e):o?o.value:t.get(e)},c=function(e,t,n,o,r){if("m"===o)throw new TypeError("Private method is not writable");if("a"===o&&!r)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof t?e!==t||!r:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===o?r.call(e,n):r?r.value=n:t.set(e,n),n};const f={type:"object"};function p(e,t){if("function"==typeof DOMException)return new DOMException(e,t);const n=new Error(e);return n.name=t,n}function m(e){if(!e||"object"!=typeof e)throw new TypeError("Tool descriptor must be an object.");if("string"!=typeof e.name||0===e.name.length)throw new TypeError("Tool descriptor requires a non-empty string 'name'.");if("string"!=typeof e.description)throw new TypeError(`Tool "${e.name}" requires a string 'description'.`);if("function"!=typeof e.execute)throw new TypeError(`Tool "${e.name}" requires an 'execute' function.`);if(void 0!==e.inputSchema&&"object"!=typeof e.inputSchema)throw new TypeError(`Tool "${e.name}": 'inputSchema' must be an object.`)}class d extends EventTarget{constructor(){super(...arguments),o.add(this),r.set(this,new Map),i.set(this,null),a.set(this,!1)}get ontoolchange(){return u(this,i,"f")}set ontoolchange(e){u(this,i,"f")&&this.removeEventListener("toolchange",u(this,i,"f")),c(this,i,"function"==typeof e?e:null,"f"),u(this,i,"f")&&this.addEventListener("toolchange",u(this,i,"f"))}registerTool(e,t={}){m(e);const{signal:n,exposedTo:i}=t;if(n?.aborted)return;u(this,o,"m",s).call(this,e.name);const a={tool:e,exposedTo:i?[...i]:void 0,pageLevel:!1};if(n){const t=()=>u(this,o,"m",s).call(this,e.name,a,!0);n.addEventListener("abort",t,{once:!0}),a.abortListener={signal:n,listener:t}}u(this,r,"f").set(e.name,a),u(this,o,"m",l).call(this)}provideContext(e={}){const t=e.tools??[];for(const e of t)m(e);if(new Set(t.map(e=>e.name)).size!==t.length)throw new TypeError("provideContext(): duplicate tool names in 'tools'.");for(const[e,t]of u(this,r,"f"))t.pageLevel&&u(this,o,"m",s).call(this,e);for(const e of t){if(u(this,r,"f").has(e.name))throw p(`provideContext(): tool "${e.name}" is already registered via registerTool().`,"InvalidStateError");u(this,r,"f").set(e.name,{tool:e,exposedTo:void 0,pageLevel:!0})}u(this,o,"m",l).call(this)}async getTools(e={}){e.fromOrigins;const t="undefined"!=typeof location?location.origin:void 0;return[...u(this,r,"f").values()].map(({tool:e})=>{const n={name:e.name,description:e.description,inputSchema:structuredClone(e.inputSchema??f)};return e.annotations&&(n.annotations=structuredClone(e.annotations)),t&&(n.origin=t),n})}async executeTool(e,t,o={}){const i=u(this,r,"f").get(e);if(!i)throw p(`No tool registered with name "${e}".`,"NotFoundError");if(o.signal?.aborted)throw p("Tool execution was aborted.","AbortError");let a;if(null==t||""===t)a={};else if("string"==typeof t)try{a=JSON.parse(t)}catch(t){throw new TypeError(`executeTool("${e}"): input is not valid JSON: ${t.message}`)}else a=t;if(null===a||"object"!=typeof a||Array.isArray(a))throw new TypeError(`executeTool("${e}"): input must be a JSON object.`);const s=n(a,i.tool.inputSchema??f);if(s.length>0){const t=s.map(e=>`${e.path}: ${e.message}`).join("; ");throw new TypeError(`executeTool("${e}"): invalid input — ${t}`)}const l=new AbortController,c=o.signal,m=()=>l.abort(c?.reason);c?.addEventListener("abort",m,{once:!0});try{const t=await i.tool.execute(a,{signal:l.signal,toolName:e});if(c?.aborted)throw p("Tool execution was aborted.","AbortError");return function(e){return null==e?{content:[]}:"string"==typeof e?{content:[{type:"text",text:e}]}:"object"==typeof e&&Array.isArray(e.content)?e:{content:[{type:"text",text:JSON.stringify(e)}]}}(t)}catch(e){if(c?.aborted||e instanceof Error&&"AbortError"===e.name)throw p("Tool execution was aborted.","AbortError");return function(e){return{isError:!0,content:[{type:"text",text:e instanceof Error?`${e.name}: ${e.message}`:String(e)}]}}(e)}finally{c?.removeEventListener("abort",m)}}unregisterTool(e){const t=u(this,o,"m",s).call(this,e);return t&&u(this,o,"m",l).call(this),t}clearContext(){if(0!==u(this,r,"f").size){for(const e of[...u(this,r,"f").keys()])u(this,o,"m",s).call(this,e);u(this,o,"m",l).call(this)}}}function h(e,t){Object.defineProperty(e,"modelContext",{value:t,writable:!1,enumerable:!0,configurable:!0})}r=new WeakMap,i=new WeakMap,a=new WeakMap,o=new WeakSet,s=function(e,t,n=!1){const i=u(this,r,"f").get(e);return!!i&&((!t||i===t)&&(i.abortListener&&i.abortListener.signal.removeEventListener("abort",i.abortListener.listener),u(this,r,"f").delete(e),n&&u(this,o,"m",l).call(this),!0))},l=function(){u(this,a,"f")||(c(this,a,!0,"f"),queueMicrotask(()=>{c(this,a,!1,"f"),this.dispatchEvent(new Event("toolchange"))}))};const g=["AskAction","SearchAction","SubscribeAction","RegisterAction","BuyAction","OrderAction","AddAction","ScheduleAction","ReserveAction","RsvpAction","ApplyAction","QuoteAction","DownloadAction","ReviewAction","CommentAction","TrackAction","CommunicateAction","InteractAction"];function v(e){return g.includes(e)}const y="https://nxis.ai/ns#";function b(e,t,n){var o;return null!==(o=e[t])&&void 0!==o?o:e[n]}function w(e,t,n){const o=b(e,t,n);return Array.isArray(o)?o:[]}function x(e=("undefined"!=typeof document?document:void 0)){const t=[];if(!e)return t;const n=e.querySelectorAll('script[type="application/ld+json"]');for(const e of Array.from(n)){const n=e.textContent;if(!n)continue;let o;try{o=JSON.parse(n)}catch(e){continue}const r=$(o);for(const e of r){const n=A(e);n&&t.push(n)}}return t}function $(e){if(Array.isArray(e)){const t=[];for(const n of e)t.push(...$(n));return t}if(e&&"object"==typeof e){const t=e;if(Array.isArray(t["@graph"])){const e=[];for(const n of t["@graph"])e.push(...$(n));return e}return[t]}return[]}function A(e){if(!e||"object"!=typeof e)return null;const t=e,n="string"==typeof t["@type"]?t["@type"]:null;if(!n)return null;if(!n.endsWith("Action")&&!v(n))return null;const o="string"==typeof t["@id"]?t["@id"]:null,r="string"==typeof t.name?t.name:null,i=function(e){if(!e||"object"!=typeof e)return null;const t=e,n="string"==typeof t.httpMethod?t.httpMethod.toUpperCase():null;if(!n)return null;const o={"@type":"EntryPoint",httpMethod:n};"string"==typeof t.encodingType&&(o.encodingType=t.encodingType);"string"==typeof t.urlTemplate&&(o.urlTemplate=t.urlTemplate);const r=b(t,"nxis:targetKind",y+"targetKind");"selfPage"!==r&&"clientSide"!==r||(o["nxis:targetKind"]=r);const i=b(t,"nxis:pageUrl",y+"pageUrl");"string"==typeof i&&(o["nxis:pageUrl"]=i);const a=b(t,"nxis:jsHandled",y+"jsHandled");!0===a&&(o["nxis:jsHandled"]=!0);return o}(t.target);if(!o||!r||!i)return null;const a=w(t,"nxis:input",y+"input").map(E).filter(e=>null!==e),s="string"==typeof t.description?t.description:function(e,t,n){const o=e.replace(/Action$/,"").toLowerCase(),r=n.map(e=>e.valueName).join(", ");if(0===n.length)return`Trigger the ${o} action "${t}".`;return`Fill the ${o} form "${t}" with fields: ${r}.`}(n,r,a);return{"@context":t["@context"],"@type":n,"@id":o,name:r,description:s,target:i,input:a.length>0?a:void 0,"nxis:extractedAt":b(t,"nxis:extractedAt",y+"extractedAt")}}function E(e){if(!e||"object"!=typeof e)return null;const t=e,n="string"==typeof t.valueName?t.valueName:null;if(!n)return null;const o=b(t,"nxis:valueType",y+"valueType"),r={"@type":"PropertyValueSpecification",valueName:n,"nxis:valueType":"string"==typeof o?o:"string"};"string"==typeof t.description&&(r.description=t.description),!0===t.valueRequired&&(r.valueRequired=!0),"string"==typeof t.defaultValue&&(r.defaultValue=t.defaultValue),"number"==typeof t.valueMinLength&&(r.valueMinLength=t.valueMinLength),"number"==typeof t.valueMaxLength&&(r.valueMaxLength=t.valueMaxLength),"number"==typeof t.minValue&&(r.minValue=t.minValue),"number"==typeof t.maxValue&&(r.maxValue=t.maxValue),"number"==typeof t.stepValue&&(r.stepValue=t.stepValue),"string"==typeof t.valuePattern&&(r.valuePattern=t.valuePattern),!0===t.multipleValues&&(r.multipleValues=!0),!0===t.readonlyValue&&(r.readonlyValue=!0);const i=w(t,"nxis:valueOption",y+"valueOption");if(i.length>0){const e=i.filter(e=>e&&"object"==typeof e&&"string"==typeof e.value).map(e=>{const t=e;return{"@type":"nxis:ValueOption",value:t.value,name:"string"==typeof t.name?t.name:void 0}});e.length>0&&(r["nxis:valueOption"]=e)}return r}function T(e){switch(e){case"string":case"date":case"file":default:return"string";case"number":return"number";case"integer":return"integer";case"boolean":return"boolean"}}function L(e){var t;const n={},o=[],r=null!==(t=e.input)&&void 0!==t?t:[];for(const e of r){const t=N(e);if(e.multipleValues){const o={type:"array",items:t};void 0!==e.valueMinLength&&(o.minItems=e.valueMinLength,delete t.minLength),void 0!==e.valueMaxLength&&(o.maxItems=e.valueMaxLength,delete t.maxLength),e.description&&(o.description=e.description,delete t.description),n[e.valueName]=o}else n[e.valueName]=t;e.valueRequired&&o.push(e.valueName)}const i={type:"object",properties:n};return o.length>0&&(i.required=o),i}function N(e){const t={type:T(e["nxis:valueType"])},n=function(e){if("date"===e)return"date"}(e["nxis:valueType"]);if(n&&(t.format=n),e.description&&(t.description=e.description),void 0!==e.defaultValue&&(t.default=function(e,t){switch(t){case"number":case"integer":{const t=Number(e);return isNaN(t)?e:t}case"boolean":return"true"===e||"1"===e;default:return e}}(e.defaultValue,e["nxis:valueType"])),void 0!==e.valueMinLength&&(t.minLength=e.valueMinLength),void 0!==e.valueMaxLength&&(t.maxLength=e.valueMaxLength),e.valuePattern){const n=e.valuePattern,o=n.startsWith("^")&&n.endsWith("$")?n:`^${n}$`;t.pattern=o}void 0!==e.minValue&&(t.minimum=e.minValue),void 0!==e.maxValue&&(t.maximum=e.maxValue),void 0!==e.stepValue&&(t.multipleOf=e.stepValue),e.readonlyValue&&(t.readOnly=!0);const o=e["nxis:valueOption"];if(o&&o.length>0){t.enum=o.map(e=>e.value);if(o.some(e=>void 0!==e.name&&e.name!==e.value)){const e=o.filter(e=>e.name&&e.name!==e.value).map(e=>`${e.value} = "${e.name}"`).join(", "),n=t.description?`${t.description}. `:"";t.description=`${n}Values: ${e}`}}return t}function k(e){const t=Array.from(document.querySelectorAll("form"));if(0===t.length)return null;const n=function(e){return e.input?e.input.map(e=>e.valueName):[]}(e);let o;if(o=!e.target.urlTemplate?t:function(e,t,n){const o=function(e){const t=e.replace(/\{[^}]*\}/g,"");try{return new URL(t,window.location.href)}catch(e){return null}}(t);return e.filter(e=>{if((e.method||"GET").toUpperCase()!==n.toUpperCase())return!1;if(!o)return!0;const t=function(e){const t=e.getAttribute("action");if(!t||"#"===t||t.startsWith("#")||""===t)return window.location.href.split("#")[0].split("?")[0];if(t.startsWith("javascript:"))return window.location.href.split("#")[0].split("?")[0];try{return new URL(t,window.location.href).href}catch(e){return t}}(e);return function(e,t){let n;try{n=new URL(e,window.location.href)}catch(e){return!1}if(n.origin!==t.origin)return!1;const o=n.pathname.replace(/\/+$/,""),r=t.pathname.replace(/\/+$/,"");return""===r||(o===r||o.startsWith(r+"/")||r.startsWith(o+"/"))}(t,o)})}(t,e.target.urlTemplate,e.target.httpMethod),0===o.length&&(o=t),1===o.length)return o[0];const r=o.map(e=>({form:e,controlNames:j(e)})).filter(({controlNames:e})=>n.every(t=>e.has(t)));return 0===r.length?(console.warn(`[nxis/webmcp] No field-set match for action "${e.name}". Expected fields: [${n.join(", ")}]. Falling back to first candidate.`),o[0]):1===r.length?r[0].form:(r.sort((e,t)=>e.controlNames.size-t.controlNames.size),r[0].controlNames.size===r[1].controlNames.size?(console.warn(`[nxis/webmcp] Ambiguous form match for action "${e.name}". ${r.length} forms have identical field sets. Returning null.`),null):r[0].form)}function j(e){const t=new Set,n=e.elements;for(let e=0;e<n.length;e++){const o=n[e];o.name&&t.add(o.name)}return t}function S(e,t){var n;return(null!==(n=e.input)&&void 0!==n?n:[]).map(e=>function(e,t){var n;const o=t=>({valueName:e.valueName,pvs:e,strategy:t}),r=function(e,t){const n=[],o=e.elements;for(let e=0;e<o.length;e++){const r=o[e];r.name===t&&n.push(r)}return n}(t,e.valueName),i=null!==(n=r[0])&&void 0!==n?n:null;if(!i)return o({kind:"skip",reason:`No control found with name="${e.valueName}"`});if(i instanceof HTMLInputElement&&"radio"===i.type){const t=r.filter(e=>e instanceof HTMLInputElement&&"radio"===e.type&&!e.disabled);return 0===t.length?o({kind:"skip",reason:`Radio group "${e.valueName}" has no enabled options`}):o({kind:"radio",name:e.valueName,elements:t})}if(i instanceof HTMLInputElement&&"checkbox"===i.type){const t=r.filter(e=>e instanceof HTMLInputElement&&"checkbox"===e.type&&!e.disabled);return 0===t.length?o({kind:"skip",reason:`Checkbox "${e.valueName}" is disabled`}):t.length>1?o({kind:"checkboxGroup",name:e.valueName,elements:t}):o({kind:"checked",element:t[0]})}if(!0===i.readOnly||null!==i.getAttribute("readonly"))return o({kind:"skip",reason:`Control "${e.valueName}" is readonly in the DOM`});if(i.disabled)return o({kind:"skip",reason:`Control "${e.valueName}" is disabled`});if(i instanceof HTMLSelectElement){const e=[];for(let t=0;t<i.options.length;t++){const n=i.options[t];n.value&&e.push(n.value)}return o(i.multiple?{kind:"multiSelect",element:i,allowedValues:e}:{kind:"select",element:i,allowedValues:e})}if(i instanceof HTMLTextAreaElement)return o({kind:"value",element:i});if(i instanceof HTMLInputElement)return o({kind:"value",element:i});return o({kind:"skip",reason:`Unsupported control type for "${e.valueName}"`})}(e,t))}async function O(e,t){var n,o;const r=function(e){const t=e.target["nxis:pageUrl"];if(!t)return null;try{const n=new URL(t),o=new URL(window.location.href);if(n.origin!==o.origin||n.pathname!==o.pathname)return`Action "${e.name}" was extracted from ${n.origin}${n.pathname} but the current page is ${o.origin}${o.pathname}. The form may have moved or changed.`}catch(e){return null}return null}(e),i=k(e);if(!i){const t=`Form not found for action "${e.name}". The page may have changed since the tool was registered.`,n={success:!1,submitted:!1,message:r?`Warning: ${r} ${t}`:t,filled:[],skipped:[]};return r&&(n.stale=!0),JSON.stringify(n)}const a=S(e,i),s=null!==(n=e.input)&&void 0!==n?n:[],l=[],u=[];let c=!1;for(const e of s)!e.valueRequired||void 0!==t[e.valueName]&&null!==t[e.valueName]||(c=!0,u.push({field:e.valueName,reason:`Required field '${e.valueName}' was not provided. Expected type: ${e["nxis:valueType"]}.`}));for(const e of a){const n=t[e.valueName];if(null==n){u.some(t=>t.field===e.valueName)||u.push({field:e.valueName,reason:"No value provided (optional field)."});continue}const o=M(e,n);!1===o.ok?u.push({field:e.valueName,reason:o.reason}):l.push(e.valueName)}await new Promise(e=>setTimeout(e,0));for(const e of a)if("value"===e.strategy.kind&&l.includes(e.valueName)){const n=e.strategy.element,r=V(null!==(o=t[e.valueName])&&void 0!==o?o:"");n.value!==r&&(u.push({field:e.valueName,reason:`Value was set to "${r}" but the DOM now shows "${n.value}". A framework may have reverted it.`}),l.splice(l.indexOf(e.valueName),1))}const f=l.length>0&&!c;let p=!1;f&&(p=function(e){try{return"function"==typeof e.requestSubmit?e.requestSubmit():e.submit(),!0}catch(e){return console.warn("[nxis/webmcp] Form submission failed:",e),!1}}(i));const m={success:f,submitted:p,message:C(l,u,p,r),filled:l,skipped:u};return r&&(m.stale=!0),JSON.stringify(m)}function C(e,t,n,o){const r=[];if(o&&r.push(`Warning: ${o}`),0===e.length&&0===t.length)return r.push("No fields were filled. Verify field names match the input schema."),r.join(" ");if(e.length>0&&r.push(`Filled ${e.length} field(s): ${e.join(", ")}.`),t.length>0){const e=t.map(e=>`${e.field} (${e.reason})`).join("; ");r.push(`Skipped ${t.length}: ${e}`)}return n?r.push("Form submitted."):e.length>0&&r.push("Form was NOT submitted due to missing required fields or fill errors."),r.join(" ")}function M(e,t){const{strategy:n}=e;switch(n.kind){case"skip":return{ok:!1,reason:n.reason};case"value":return function(e,t){var n;const o=null===(n=Object.getOwnPropertyDescriptor(e instanceof HTMLTextAreaElement?HTMLTextAreaElement.prototype:HTMLInputElement.prototype,"value"))||void 0===n?void 0:n.set;o?o.call(e,t):e.value=t;return e.dispatchEvent(new Event("input",{bubbles:!0})),e.dispatchEvent(new Event("change",{bubbles:!0})),{ok:!0}}(n.element,V(t));case"checked":return function(e,t){return e.checked=function(e){if("boolean"==typeof e)return e;if("number"==typeof e)return 0!==e;if("string"==typeof e){const t=e.trim().toLowerCase();return""!==t&&"false"!==t&&"0"!==t&&"off"!==t&&"no"!==t}return Boolean(e)}(t),e.dispatchEvent(new Event("change",{bubbles:!0})),{ok:!0}}(n.element,t);case"radio":return function(e,t){var n,o;const r=String(t),i=null!==(o=null!==(n=e.find(e=>e.value===r))&&void 0!==n?n:e.find(e=>e.value.toLowerCase()===r.toLowerCase()))&&void 0!==o?o:e.find(e=>function(e){var t;if(e.labels&&e.labels.length>0)return(null!==(t=e.labels[0].textContent)&&void 0!==t?t:"").trim();return e.value}(e).toLowerCase()===r.toLowerCase());if(!i){const t=e.map(e=>e.value).join(", ");return{ok:!1,reason:`Value "${r}" is not an allowed option for radio "${e[0].name}". Allowed: [${t}].`}}return i.checked=!0,i.dispatchEvent(new Event("input",{bubbles:!0})),i.dispatchEvent(new Event("change",{bubbles:!0})),{ok:!0}}(n.elements,t);case"checkboxGroup":return function(e,t){const n=(Array.isArray(t)?t:[t]).map(e=>String(e)),o=n.map(e=>e.toLowerCase());let r=!1;for(const t of e){const e=n.includes(t.value)||o.includes(t.value.toLowerCase());t.checked=e,e&&(r=!0),t.dispatchEvent(new Event("change",{bubbles:!0}))}if(!r){const t=e.map(e=>e.value).join(", ");return{ok:!1,reason:`None of [${n.join(", ")}] match checkbox group "${e[0].name}". Allowed: [${t}].`}}return{ok:!0}}(n.elements,t);case"select":return function(e,t,n){const o=P(e,t,n);if(null===o)return{ok:!1,reason:`Value "${t}" is not an allowed option for "${e.name}". Allowed: [${n.join(", ")}].`};return e.value=o,e.dispatchEvent(new Event("input",{bubbles:!0})),e.dispatchEvent(new Event("change",{bubbles:!0})),{ok:!0}}(n.element,String(t),n.allowedValues);case"multiSelect":return function(e,t,n){const o=(Array.isArray(t)?t:[t]).map(e=>String(e)),r=new Set,i=[];for(const t of o){const o=P(e,t,n);null===o?i.push(t):r.add(o)}if(0===r.size)return{ok:!1,reason:`None of [${o.join(", ")}] are allowed options for "${e.name}". Allowed: [${n.join(", ")}].`};for(const t of Array.from(e.options))t.selected=r.has(t.value);if(e.dispatchEvent(new Event("input",{bubbles:!0})),e.dispatchEvent(new Event("change",{bubbles:!0})),i.length>0)return{ok:!1,reason:`Selected [${Array.from(r).join(", ")}] but [${i.join(", ")}] are not allowed options for "${e.name}". Allowed: [${n.join(", ")}].`};return{ok:!0}}(n.element,t,n.allowedValues);default:return{ok:!1,reason:`Unsupported fill strategy for field "${e.valueName}".`}}}function V(e){return Array.isArray(e)?e.map(e=>String(e)).join(","):String(e)}function P(e,t,n){if(n.includes(t))return t;const o=n.find(e=>e.toLowerCase()===t.toLowerCase());if(o)return o;for(const n of Array.from(e.options))if(n.text.toLowerCase()===t.toLowerCase())return n.value;return null}function q(e){var t;const n=!1!==(null==e?void 0:e.observe),o=null!==(t=null==e?void 0:e.doc)&&void 0!==t?t:"undefined"!=typeof document?document:void 0;if(!o)return console.warn("[nxis/webmcp] No document available (not running in a browser). Tools will not be registered."),()=>{};!function(e={}){const{force:t=!1,installNavigatorAlias:n=!0}=e,o="undefined"!=typeof document&&document.modelContext||"undefined"!=typeof navigator&&navigator.modelContext||void 0;if(o&&!t)return o;const r=new d;"undefined"!=typeof document&&h(document,r),n&&"undefined"!=typeof navigator&&h(navigator,r)}();const r=function(e){var t,n;const o=null!==(n=null===(t=e.defaultView)||void 0===t?void 0:t.navigator)&&void 0!==n?n:"undefined"!=typeof navigator?navigator:void 0;return e.modelContext||(null==o?void 0:o.modelContext)||null}(o);if(!r)return console.warn("[nxis/webmcp] WebMCP API not available — document.modelContext is undefined. Tools will not be registered."),()=>{};const i=new AbortController;(null==e?void 0:e.signal)&&(e.signal.aborted?i.abort():e.signal.addEventListener("abort",()=>i.abort()));const a={ids:new Set,names:new Set};I(o,r,i,a);let s=null;return n&&(s=function(e,t,n,o){const r=new MutationObserver(r=>{(function(e){for(const t of e)for(const e of Array.from(t.addedNodes)){if(H(e))return!0;if(e instanceof Element&&e.querySelector('script[type="application/ld+json"]'))return!0}return!1})(r)&&I(e,t,n,o)});return n.signal.addEventListener("abort",()=>r.disconnect()),r.observe(e.documentElement,{childList:!0,subtree:!0}),r}(o,r,i,a)),()=>{i.abort(),s&&(s.disconnect(),s=null)}}function I(e,t,n,o){const r=x(e);let i=0;for(const e of r)if(!o.ids.has(e["@id"]))try{R(e,t,n,o),o.ids.add(e["@id"]),i++}catch(t){console.warn(`[nxis/webmcp] Failed to register tool for action "${e.name}":`,t)}i>0&&console.info(`[nxis/webmcp] Registered ${i} tool(s) (${o.ids.size} total) from ${r.length} action(s).`)}function R(e,t,n,o){const r=L(e),i=function(e,t){if(!t.has(e))return e;for(let n=2;;n++){const o=`_${n}`,r=e.slice(0,64-o.length)+o;if(!t.has(r))return r}}(function(e){return e.toLowerCase().replace(/[\s-]+/g,"_").replace(/[^a-z0-9_]/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,"").slice(0,64)||"unnamed_action"}(e.name),o.names);t.registerTool({name:i,description:e.description,inputSchema:r,annotations:{readOnlyHint:!1,untrustedContentHint:!0},execute:async t=>O(e,t)},{signal:n.signal}),o.names.add(i)}function H(e){return e instanceof HTMLScriptElement&&"application/ld+json"===e.type}export{g as ACTION_SUBTYPES,L as buildInputSchema,v as isActionType,x as readActions,q as registerActions};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxisai/nxis",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Javascript client for Nxis AI",
|
|
5
5
|
"main": "build/index.cjs.js",
|
|
6
6
|
"module": "build/index.esm.js",
|
|
@@ -17,10 +17,14 @@
|
|
|
17
17
|
"import": "./build/index.esm.js",
|
|
18
18
|
"require": "./build/index.cjs.js",
|
|
19
19
|
"types": "./build/index.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./webmcp": {
|
|
22
|
+
"browser": "./build/webmcp.js",
|
|
23
|
+
"types": "./build/webmcp.d.ts"
|
|
20
24
|
}
|
|
21
25
|
},
|
|
22
26
|
"scripts": {
|
|
23
|
-
"build:types": "tsc --declaration --emitDeclarationOnly --outDir build --target es2018 --skipLibCheck --moduleResolution node --esModuleInterop src/index.ts src/server.ts src/core.ts src/api.ts src/auth.ts",
|
|
27
|
+
"build:types": "tsc --declaration --emitDeclarationOnly --outDir build --target es2018 --skipLibCheck --moduleResolution node --esModuleInterop src/index.ts src/server.ts src/core.ts src/api.ts src/auth.ts src/webmcp/index.ts",
|
|
24
28
|
"build": "source .env && npx rollup --config rollup.config.js --environment NXIS_API_HOST:$NXIS_API_HOST && npm run build:types",
|
|
25
29
|
"build:dev": "source .env && npx rollup --config rollup.config.js --environment NXIS_API_HOST:$NXIS_API_HOST && npm run build:types",
|
|
26
30
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -42,6 +46,7 @@
|
|
|
42
46
|
"rollup-plugin-uglify": "^4.0.0"
|
|
43
47
|
},
|
|
44
48
|
"dependencies": {
|
|
49
|
+
"@nxisai/webmcp": "^0.1.0",
|
|
45
50
|
"tslib": "^2.3.1",
|
|
46
51
|
"typescript": "^4.4.3",
|
|
47
52
|
"uuid": "^14.0.0",
|
|
@@ -49,5 +54,8 @@
|
|
|
49
54
|
},
|
|
50
55
|
"overrides": {
|
|
51
56
|
"serialize-javascript": "^7.0.5"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"registry": "https://registry.npmjs.org"
|
|
52
60
|
}
|
|
53
61
|
}
|
package/rollup.config.js
CHANGED
|
@@ -47,4 +47,23 @@ const ssrConfig = {
|
|
|
47
47
|
external: ["uuid"],
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
// WebMCP bundle — browser-only ES module for Action tool registration
|
|
51
|
+
const webmcpConfig = {
|
|
52
|
+
input: "src/webmcp/index.ts",
|
|
53
|
+
output: {
|
|
54
|
+
file: "build/webmcp.js",
|
|
55
|
+
format: "es",
|
|
56
|
+
},
|
|
57
|
+
plugins: [
|
|
58
|
+
resolve({
|
|
59
|
+
browser: true,
|
|
60
|
+
customResolveOptions: {
|
|
61
|
+
moduleDirectory: "node_modules",
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
typescript({ declaration: false }),
|
|
65
|
+
terser(),
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default [browserConfig, ssrConfig, webmcpConfig];
|
package/src/api.ts
CHANGED
|
@@ -37,6 +37,19 @@ export type NxisConfig = {
|
|
|
37
37
|
site_id: string
|
|
38
38
|
/** The subscription identifier used for quota and entitlement checks. */
|
|
39
39
|
sub_id: string
|
|
40
|
+
/**
|
|
41
|
+
* WebMCP configuration. Omit entirely to use defaults (`mode: "browser"`,
|
|
42
|
+
* `declarative: false`).
|
|
43
|
+
*/
|
|
44
|
+
webmcp?: {
|
|
45
|
+
/**
|
|
46
|
+
* Integration mode:
|
|
47
|
+
* - `"browser"` (default): Fetch JSON-LD from API, inject into page, register WebMCP tools.
|
|
48
|
+
* - `"ssr"`: Skip fetch/inject (server already rendered JSON-LD), only register WebMCP tools from existing DOM.
|
|
49
|
+
* - `"none"`: Fetch and inject JSON-LD for SEO, but do not register WebMCP tools.
|
|
50
|
+
*/
|
|
51
|
+
mode: "browser" | "ssr" | "none";
|
|
52
|
+
}
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
/**
|
|
@@ -52,8 +65,6 @@ export type StructuredDataRequest = {
|
|
|
52
65
|
uri: HttpUri
|
|
53
66
|
/** The `document.referrer` value, if available. */
|
|
54
67
|
referrer?: string
|
|
55
|
-
/** A client-generated session ID for request correlation. */
|
|
56
|
-
session?: string
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
/**
|
package/src/client.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { NxisWarning, NxisConfig, StructuredDataResponse } from './api';
|
|
2
2
|
import { buildParams, responseHandler } from './core';
|
|
3
|
-
import {
|
|
3
|
+
import { registerActions } from './webmcp/register';
|
|
4
4
|
|
|
5
5
|
declare global {
|
|
6
6
|
interface Window {
|
|
7
|
-
/** A unique session ID generated per page load for request correlation. */
|
|
8
|
-
NXIS_SESSION_ID: string;
|
|
9
7
|
/**
|
|
10
8
|
* Configuration injected by the Nxis CDN into the served script.
|
|
11
9
|
* Contains organization, site, and subscription identifiers extracted
|
|
@@ -70,18 +68,25 @@ function errorHandler(e: Error): void {
|
|
|
70
68
|
* This is the main entry point for the browser bundle (`build/nxis.js`).
|
|
71
69
|
* It performs the following steps:
|
|
72
70
|
*
|
|
73
|
-
* 1.
|
|
74
|
-
* 2.
|
|
75
|
-
* 3.
|
|
76
|
-
*
|
|
77
|
-
* 4. Calls the `/v1/nxis:inject` proxy endpoint.
|
|
78
|
-
* 5. On success, injects the returned JSON-LD into the document `<head>`.
|
|
71
|
+
* 1. Reads the injected {@link NxisConfig} from `window.__NXIS_CONFIG__`.
|
|
72
|
+
* 2. Builds the request parameters from the current page URL and referrer.
|
|
73
|
+
* 3. Calls the `/v1/nxis:inject` proxy endpoint.
|
|
74
|
+
* 4. On success, injects the returned JSON-LD into the document `<head>`.
|
|
79
75
|
* 6. On failure, logs a warning or error to the console.
|
|
80
76
|
*/
|
|
81
77
|
(function () {
|
|
82
|
-
window.NXIS_SESSION_ID = uuidv4();
|
|
83
|
-
const API = process.env.NXIS_API_HOST;
|
|
84
78
|
const config: NxisConfig = window.__NXIS_CONFIG__;
|
|
79
|
+
const mode = config?.webmcp?.mode ?? 'browser';
|
|
80
|
+
|
|
81
|
+
// SSR mode: JSON-LD is already in the DOM from server rendering.
|
|
82
|
+
// Skip the fetch entirely — just register WebMCP tools from existing markup.
|
|
83
|
+
if (mode === 'ssr') {
|
|
84
|
+
registerActions();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Browser + None modes: fetch and inject JSON-LD from the API
|
|
89
|
+
const API = process.env.NXIS_API_HOST;
|
|
85
90
|
|
|
86
91
|
const headers: Headers = new Headers();
|
|
87
92
|
const NXIS: URL = new URL(`${API}/v1/nxis:inject`);
|
|
@@ -91,7 +96,6 @@ function errorHandler(e: Error): void {
|
|
|
91
96
|
site_id: { value: config?.site_id },
|
|
92
97
|
uri: new URL(window.location.href),
|
|
93
98
|
referrer: document.referrer,
|
|
94
|
-
session: window.NXIS_SESSION_ID
|
|
95
99
|
}).toString();
|
|
96
100
|
if (config?.sub_id) headers.set("X-NXIS-SUBSCRIPTION", config.sub_id);
|
|
97
101
|
headers.set("Content-Type", "application/json");
|
|
@@ -99,6 +103,12 @@ function errorHandler(e: Error): void {
|
|
|
99
103
|
|
|
100
104
|
fetch(`${NXIS}`, { headers })
|
|
101
105
|
.then((res) => responseHandler(res, window.location.href))
|
|
102
|
-
.then(
|
|
106
|
+
.then((response) => {
|
|
107
|
+
inject(response);
|
|
108
|
+
// Only register WebMCP tools in "browser" mode
|
|
109
|
+
if (mode === 'browser') {
|
|
110
|
+
registerActions();
|
|
111
|
+
}
|
|
112
|
+
})
|
|
103
113
|
.catch(errorHandler);
|
|
104
114
|
})();
|
package/src/core.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { StructuredDataRequest, NxisWarning, StructuredDataResponse, NxisErrorRe
|
|
|
5
5
|
* the client-side `/v1/nxis:inject` endpoint.
|
|
6
6
|
*
|
|
7
7
|
* @param request - The structured data request containing org, site, URI,
|
|
8
|
-
*
|
|
8
|
+
* and referrer info.
|
|
9
9
|
* @returns A `URLSearchParams` instance ready to be appended to the request URL.
|
|
10
10
|
*
|
|
11
11
|
* @internal
|
|
@@ -16,7 +16,6 @@ export function buildParams(request: StructuredDataRequest): URLSearchParams {
|
|
|
16
16
|
if (request.site_id?.value) output.append("site_id", request.site_id.value);
|
|
17
17
|
output.append("uri", request.uri.href);
|
|
18
18
|
if (request.referrer) output.append("referrer", request.referrer);
|
|
19
|
-
if (request.session) output.append("session", request.session);
|
|
20
19
|
return output;
|
|
21
20
|
}
|
|
22
21
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-shape types matching the backend JsonLdEmitter output.
|
|
3
|
+
*
|
|
4
|
+
* These types describe the JSON-LD that NxisAI's Action Specialist emits
|
|
5
|
+
* into `<script type="application/ld+json">` blocks. They are the single
|
|
6
|
+
* source of truth for the frontend consumer — any change here is a
|
|
7
|
+
* versioned contract change.
|
|
8
|
+
*
|
|
9
|
+
* @see JsonLdEmitter.scala in nxis-autofill-runner
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Value types — closed set derived from HTML input[type]
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** The closed set of inferred value types emitted by the backend. */
|
|
18
|
+
export type NxisValueType =
|
|
19
|
+
| "string"
|
|
20
|
+
| "number"
|
|
21
|
+
| "integer"
|
|
22
|
+
| "boolean"
|
|
23
|
+
| "date"
|
|
24
|
+
| "file";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// PropertyValueSpecification
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** A single option value for select/radio fields. */
|
|
31
|
+
export interface NxisValueOption {
|
|
32
|
+
"@type": "nxis:ValueOption";
|
|
33
|
+
value: string;
|
|
34
|
+
/** Human-readable label. Absent when label === value. */
|
|
35
|
+
name?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Mirrors schema.org PropertyValueSpecification + nxis extensions.
|
|
40
|
+
*
|
|
41
|
+
* Every field is an inferred descriptor — the consumer verifies
|
|
42
|
+
* against the live DOM control.
|
|
43
|
+
*/
|
|
44
|
+
export interface NxisPropertyValueSpecification {
|
|
45
|
+
"@type": "PropertyValueSpecification";
|
|
46
|
+
/** REQUIRED — property key, maps to form control `name`. */
|
|
47
|
+
valueName: string;
|
|
48
|
+
/** REQUIRED — inferred from HTML input[type]; consumer verifies against DOM. */
|
|
49
|
+
"nxis:valueType": NxisValueType;
|
|
50
|
+
/** Human-readable description (from label or placeholder). */
|
|
51
|
+
description?: string;
|
|
52
|
+
/** Inferred from HTML `required` attribute. */
|
|
53
|
+
valueRequired?: boolean;
|
|
54
|
+
/** Inferred from HTML `value` attribute. */
|
|
55
|
+
defaultValue?: string;
|
|
56
|
+
/** Inferred from HTML `minlength`. */
|
|
57
|
+
valueMinLength?: number;
|
|
58
|
+
/** Inferred from HTML `maxlength`. */
|
|
59
|
+
valueMaxLength?: number;
|
|
60
|
+
/** Inferred from HTML `min`. */
|
|
61
|
+
minValue?: number;
|
|
62
|
+
/** Inferred from HTML `max`. */
|
|
63
|
+
maxValue?: number;
|
|
64
|
+
/** Inferred from HTML `step`. */
|
|
65
|
+
stepValue?: number;
|
|
66
|
+
/** Inferred from HTML `pattern` — emitted UNANCHORED; consumer anchors with ^…$. */
|
|
67
|
+
valuePattern?: string;
|
|
68
|
+
/** Inferred from HTML `multiple`. */
|
|
69
|
+
multipleValues?: boolean;
|
|
70
|
+
/** Inferred from HTML `readonly`. */
|
|
71
|
+
readonlyValue?: boolean;
|
|
72
|
+
/** REQUIRED for select/radio — actual option values from DOM. */
|
|
73
|
+
"nxis:valueOption"?: NxisValueOption[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// EntryPoint — carries the SubmitTarget discriminator in the JSON-LD shape
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Mirrors schema.org EntryPoint + nxis extensions.
|
|
82
|
+
*
|
|
83
|
+
* The backend's `SubmitTarget` discriminator is expressed as:
|
|
84
|
+
* - `urlTemplate` present → Url target (real server endpoint)
|
|
85
|
+
* - `nxis:targetKind` present → endpointless (SelfPage or ClientSide)
|
|
86
|
+
*
|
|
87
|
+
* These are mutually exclusive in the emitted JSON-LD.
|
|
88
|
+
*/
|
|
89
|
+
export interface NxisEntryPoint {
|
|
90
|
+
"@type": "EntryPoint";
|
|
91
|
+
/** Form method (GET or POST), uppercase. */
|
|
92
|
+
httpMethod: string;
|
|
93
|
+
/** Form enctype (e.g., "application/x-www-form-urlencoded"). */
|
|
94
|
+
encodingType?: string;
|
|
95
|
+
|
|
96
|
+
// --- URL target (SubmitTarget.Url) ---
|
|
97
|
+
/** Real, resolvable server endpoint. May include RFC 6570 template vars for GET. */
|
|
98
|
+
urlTemplate?: string;
|
|
99
|
+
|
|
100
|
+
// --- Endpointless target (SubmitTarget.SelfPage / ClientSide) ---
|
|
101
|
+
/** Discriminator for endpointless forms. */
|
|
102
|
+
"nxis:targetKind"?: "selfPage" | "clientSide";
|
|
103
|
+
/** Page URL for staleness checking. */
|
|
104
|
+
"nxis:pageUrl"?: string;
|
|
105
|
+
/** Evidence that a JS submit/click listener exists on the form. */
|
|
106
|
+
"nxis:jsHandled"?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Action — the top-level entity
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A schema.org Action node as emitted by the backend JsonLdEmitter.
|
|
115
|
+
*
|
|
116
|
+
* Found in `<script type="application/ld+json">` blocks, either standalone
|
|
117
|
+
* or nested within a `@graph` array alongside other schema.org types.
|
|
118
|
+
*/
|
|
119
|
+
export interface NxisAction {
|
|
120
|
+
"@context": unknown;
|
|
121
|
+
/** Action or a subtype (e.g., "SearchAction", "CommunicateAction"). */
|
|
122
|
+
"@type": string;
|
|
123
|
+
/** Provenance/identity URN. NOT a DOM locator. */
|
|
124
|
+
"@id": string;
|
|
125
|
+
/** Human-readable action name (becomes the tool name). */
|
|
126
|
+
name: string;
|
|
127
|
+
/** Human-readable action description (becomes the tool description). */
|
|
128
|
+
description: string;
|
|
129
|
+
/** EntryPoint describing how to invoke the action. */
|
|
130
|
+
target: NxisEntryPoint;
|
|
131
|
+
/** Inferred field descriptors (may be empty for actions with no inputs). */
|
|
132
|
+
input?: NxisPropertyValueSpecification[];
|
|
133
|
+
/** Crawl timestamp — consumer uses to assess staleness. */
|
|
134
|
+
"nxis:extractedAt"?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Known Action subtypes
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
/** The set of Action subtypes the backend classifier emits. */
|
|
142
|
+
export const ACTION_SUBTYPES = [
|
|
143
|
+
"AskAction",
|
|
144
|
+
"SearchAction",
|
|
145
|
+
"SubscribeAction",
|
|
146
|
+
"RegisterAction",
|
|
147
|
+
"BuyAction",
|
|
148
|
+
"OrderAction",
|
|
149
|
+
"AddAction",
|
|
150
|
+
"ScheduleAction",
|
|
151
|
+
"ReserveAction",
|
|
152
|
+
"RsvpAction",
|
|
153
|
+
"ApplyAction",
|
|
154
|
+
"QuoteAction",
|
|
155
|
+
"DownloadAction",
|
|
156
|
+
"ReviewAction",
|
|
157
|
+
"CommentAction",
|
|
158
|
+
"TrackAction",
|
|
159
|
+
"CommunicateAction",
|
|
160
|
+
"InteractAction",
|
|
161
|
+
] as const;
|
|
162
|
+
|
|
163
|
+
export type ActionSubtype = typeof ACTION_SUBTYPES[number];
|
|
164
|
+
|
|
165
|
+
/** Check if a string is a known Action subtype. */
|
|
166
|
+
export function isActionType(type: string): type is ActionSubtype {
|
|
167
|
+
return (ACTION_SUBTYPES as readonly string[]).includes(type);
|
|
168
|
+
}
|