@nevescloud/pip 3.3.0 → 3.4.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/package.json +4 -1
- package/pip-core.esm.js +12 -5
- package/webmcp.esm.js +211 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nevescloud/pip",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.1",
|
|
4
4
|
"description": "Floating assistant bubble + panel + chat runtime. ESM, no build.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "pip-core.esm.js",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
".": "./pip-core.esm.js",
|
|
9
9
|
"./pip-core.esm.js": "./pip-core.esm.js",
|
|
10
10
|
"./runtime.esm.js": "./runtime.esm.js",
|
|
11
|
+
"./webmcp": "./webmcp.esm.js",
|
|
12
|
+
"./webmcp.esm.js": "./webmcp.esm.js",
|
|
11
13
|
"./bundle": "./bundle/anthropic.esm.js",
|
|
12
14
|
"./bundle/anthropic": "./bundle/anthropic.esm.js",
|
|
13
15
|
"./bundle/anthropic.esm.js": "./bundle/anthropic.esm.js",
|
|
@@ -22,6 +24,7 @@
|
|
|
22
24
|
"files": [
|
|
23
25
|
"pip-core.esm.js",
|
|
24
26
|
"runtime.esm.js",
|
|
27
|
+
"webmcp.esm.js",
|
|
25
28
|
"bundle/",
|
|
26
29
|
"providers/",
|
|
27
30
|
"README.md",
|
package/pip-core.esm.js
CHANGED
|
@@ -2039,10 +2039,11 @@ export function createPip(opts = {}) {
|
|
|
2039
2039
|
slashList.innerHTML = "";
|
|
2040
2040
|
if (!slashCurrent.length) { slashList.hidden = true; scroll.classList.remove("is-backdrop"); return; }
|
|
2041
2041
|
scroll.classList.add("is-backdrop");
|
|
2042
|
+
let selectedLi = null;
|
|
2042
2043
|
slashCurrent.forEach((item, i) => {
|
|
2043
2044
|
const li = document.createElement("li");
|
|
2044
2045
|
li.setAttribute("role", "option");
|
|
2045
|
-
if (i === slashSelected) li.classList.add("selected");
|
|
2046
|
+
if (i === slashSelected) { li.classList.add("selected"); selectedLi = li; }
|
|
2046
2047
|
const name = document.createElement("span");
|
|
2047
2048
|
name.className = "name";
|
|
2048
2049
|
name.textContent = item.isArg ? item.name : `/${item.name}`;
|
|
@@ -2054,10 +2055,12 @@ export function createPip(opts = {}) {
|
|
|
2054
2055
|
li.appendChild(desc);
|
|
2055
2056
|
}
|
|
2056
2057
|
// Mouse hover promotes the row to the keyboard selection so Enter
|
|
2057
|
-
// and arrow keys
|
|
2058
|
-
//
|
|
2059
|
-
// the
|
|
2060
|
-
|
|
2058
|
+
// and arrow keys agree with the visible highlight. Listen on
|
|
2059
|
+
// mousemove (not mouseenter) so a stationary cursor doesn't grab
|
|
2060
|
+
// selection when the keyboard scrolls a new row under it — the
|
|
2061
|
+
// re-render replaces the DOM, which would re-fire mouseenter on
|
|
2062
|
+
// the new element under the cursor and undo the arrow keypress.
|
|
2063
|
+
li.addEventListener("mousemove", () => {
|
|
2061
2064
|
if (slashSelected === i) return;
|
|
2062
2065
|
slashSelected = i;
|
|
2063
2066
|
for (const sib of slashList.children) sib.classList.remove("selected");
|
|
@@ -2071,6 +2074,10 @@ export function createPip(opts = {}) {
|
|
|
2071
2074
|
slashList.appendChild(li);
|
|
2072
2075
|
});
|
|
2073
2076
|
slashList.hidden = false;
|
|
2077
|
+
// Keep the keyboard-driven selection in view. block:"nearest" only
|
|
2078
|
+
// scrolls when the selected row is actually clipped — no jarring
|
|
2079
|
+
// re-center when it's already visible.
|
|
2080
|
+
selectedLi?.scrollIntoView({ block: "nearest" });
|
|
2074
2081
|
}
|
|
2075
2082
|
|
|
2076
2083
|
function closeSlashSuggest() {
|
package/webmcp.esm.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// WebMCP adapter for pip-runtime.
|
|
2
|
+
//
|
|
3
|
+
// Discovers tools registered via the standard `navigator.modelContext`
|
|
4
|
+
// surface (W3C Community Group Report, 23 April 2026) and wires each one
|
|
5
|
+
// into pip's runtime tool registry. Tools registered on the host page
|
|
6
|
+
// become tools the runtime's provider can call.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// import { createRuntime } from '@nevescloud/pip/runtime.esm.js';
|
|
10
|
+
// import { attachWebmcp } from '@nevescloud/pip/webmcp.esm.js';
|
|
11
|
+
//
|
|
12
|
+
// const rt = createRuntime({ provider });
|
|
13
|
+
// const webmcp = attachWebmcp(rt);
|
|
14
|
+
//
|
|
15
|
+
// // Page code (somewhere) calls:
|
|
16
|
+
// navigator.modelContext.registerTool({
|
|
17
|
+
// name: 'get_greeting',
|
|
18
|
+
// description: 'Returns a fresh greeting from this tab.',
|
|
19
|
+
// inputSchema: { type: 'object', properties: {}, required: [] },
|
|
20
|
+
// async execute() { return 'Hello!'; },
|
|
21
|
+
// });
|
|
22
|
+
//
|
|
23
|
+
// // The runtime's provider now sees `get_greeting` as a callable tool.
|
|
24
|
+
//
|
|
25
|
+
// // Clean up (reverses the patch, unregisters mirrored tools):
|
|
26
|
+
// webmcp.stop();
|
|
27
|
+
//
|
|
28
|
+
// Discovery strategy:
|
|
29
|
+
//
|
|
30
|
+
// 1. **Snapshot at attach time.** If `navigator.modelContext` exposes an
|
|
31
|
+
// iterable of currently-registered tools (`mc.tools`, `mc.getTools()`,
|
|
32
|
+
// or similar), we read it and mirror each entry into runtime. This
|
|
33
|
+
// covers tools registered before the adapter started.
|
|
34
|
+
// 2. **Intercept future calls.** `registerTool` / `unregisterTool` on
|
|
35
|
+
// `navigator.modelContext` are patched so any later call from the
|
|
36
|
+
// same realm propagates to runtime automatically.
|
|
37
|
+
//
|
|
38
|
+
// Limitations:
|
|
39
|
+
//
|
|
40
|
+
// - Tools registered from a *different* JavaScript realm (e.g. a separate
|
|
41
|
+
// extension's content script with its own `navigator` reference) are
|
|
42
|
+
// invisible to the patch. Snapshot covers them only if `mc` exposes
|
|
43
|
+
// introspection.
|
|
44
|
+
// - The WebMCP draft is still in flux; the field names tools may use
|
|
45
|
+
// (`execute` vs `handler`, `inputSchema` vs `schema`, etc.) are
|
|
46
|
+
// normalized below — both shapes are accepted.
|
|
47
|
+
|
|
48
|
+
const DEFAULT_SCHEMA = { type: 'object', properties: {}, required: [] };
|
|
49
|
+
|
|
50
|
+
// Pull a callable executor off the WebMCP tool def under any of the names
|
|
51
|
+
// the spec/implementations currently use. Returning null lets the caller
|
|
52
|
+
// emit a clearer error than "undefined is not a function".
|
|
53
|
+
function pickExecutor(toolDef) {
|
|
54
|
+
return (
|
|
55
|
+
toolDef.execute ||
|
|
56
|
+
toolDef.handler ||
|
|
57
|
+
toolDef.run ||
|
|
58
|
+
null
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Some WebMCP impls hand the input through under `input`, others as a
|
|
63
|
+
// positional argument. The runtime always calls handler(input, ctx), so
|
|
64
|
+
// pass input through positionally and trust the executor's own shape.
|
|
65
|
+
function makeRuntimeHandler(toolDef) {
|
|
66
|
+
const exec = pickExecutor(toolDef);
|
|
67
|
+
if (!exec) {
|
|
68
|
+
return async () => ({
|
|
69
|
+
error: `[webmcp] tool "${toolDef.name}" has no execute()/handler()`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return async (input, ctx) => {
|
|
73
|
+
// ctx.signal exists on runtime tools but WebMCP execute() may not
|
|
74
|
+
// expect a signal in its signature — pass through only if the
|
|
75
|
+
// executor declares ≥2 parameters. Conservative: forward signal as
|
|
76
|
+
// a sibling property on the input where possible.
|
|
77
|
+
try {
|
|
78
|
+
return await exec(input, ctx);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return { error: String(err?.message || err) };
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toRuntimeToolDef(toolDef) {
|
|
86
|
+
return {
|
|
87
|
+
name: toolDef.name,
|
|
88
|
+
description: toolDef.description || '',
|
|
89
|
+
schema: toolDef.inputSchema || toolDef.schema || DEFAULT_SCHEMA,
|
|
90
|
+
handler: makeRuntimeHandler(toolDef),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Try to read the current WebMCP tool registry without committing to a
|
|
95
|
+
// single shape — different impls expose this differently.
|
|
96
|
+
function snapshotExisting(mc) {
|
|
97
|
+
if (Array.isArray(mc.tools)) return [...mc.tools];
|
|
98
|
+
if (typeof mc.getTools === 'function') {
|
|
99
|
+
try {
|
|
100
|
+
const t = mc.getTools();
|
|
101
|
+
if (Array.isArray(t)) return t;
|
|
102
|
+
} catch { /* swallow */ }
|
|
103
|
+
}
|
|
104
|
+
// No introspection available — best we can do is empty. Subsequent
|
|
105
|
+
// registerTool calls will be caught by the patch.
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function attachWebmcp(runtime, options = {}) {
|
|
110
|
+
if (!runtime || typeof runtime.registerTool !== 'function') {
|
|
111
|
+
throw new Error('[webmcp] attachWebmcp(runtime): runtime must expose registerTool/unregisterTool (createRuntime() handle).');
|
|
112
|
+
}
|
|
113
|
+
if (typeof navigator === 'undefined' || !navigator.modelContext) {
|
|
114
|
+
throw new Error('[webmcp] navigator.modelContext is not available. Requires a supporting browser (Chrome 146+ with the WebMCP flag, or a polyfill such as hatch).');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const {
|
|
118
|
+
autoRegisterExisting = true,
|
|
119
|
+
namePrefix = '',
|
|
120
|
+
onWire = null,
|
|
121
|
+
onUnwire = null,
|
|
122
|
+
} = options;
|
|
123
|
+
|
|
124
|
+
const mc = navigator.modelContext;
|
|
125
|
+
// wired: webmcp tool name (as seen by mc) → runtime tool name (post-prefix).
|
|
126
|
+
const wired = new Map();
|
|
127
|
+
const originalRegister = typeof mc.registerTool === 'function' ? mc.registerTool.bind(mc) : null;
|
|
128
|
+
const originalUnregister = typeof mc.unregisterTool === 'function' ? mc.unregisterTool.bind(mc) : null;
|
|
129
|
+
let patched = false;
|
|
130
|
+
let stopped = false;
|
|
131
|
+
|
|
132
|
+
function runtimeName(webmcpName) {
|
|
133
|
+
return namePrefix ? `${namePrefix}${webmcpName}` : webmcpName;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function wireOne(toolDef) {
|
|
137
|
+
if (!toolDef || !toolDef.name) return;
|
|
138
|
+
if (wired.has(toolDef.name)) return; // already mirrored
|
|
139
|
+
const rtDef = toRuntimeToolDef(toolDef);
|
|
140
|
+
rtDef.name = runtimeName(toolDef.name);
|
|
141
|
+
try {
|
|
142
|
+
runtime.registerTool(rtDef);
|
|
143
|
+
wired.set(toolDef.name, rtDef.name);
|
|
144
|
+
onWire?.(rtDef.name, toolDef);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
// Surface but don't throw — one bad tool shouldn't break the rest.
|
|
147
|
+
console.warn(`[webmcp] could not mirror tool "${toolDef.name}":`, err?.message || err);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function unwireOne(webmcpName) {
|
|
152
|
+
const rtName = wired.get(webmcpName);
|
|
153
|
+
if (!rtName) return;
|
|
154
|
+
try { runtime.unregisterTool(rtName); } catch {}
|
|
155
|
+
wired.delete(webmcpName);
|
|
156
|
+
onUnwire?.(rtName, webmcpName);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function patchModelContext() {
|
|
160
|
+
if (patched || !originalRegister) return;
|
|
161
|
+
mc.registerTool = function patchedRegisterTool(toolDef) {
|
|
162
|
+
const result = originalRegister(toolDef);
|
|
163
|
+
try { wireOne(toolDef); } catch (err) {
|
|
164
|
+
console.warn('[webmcp] wireOne failed:', err?.message || err);
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
};
|
|
168
|
+
if (originalUnregister) {
|
|
169
|
+
mc.unregisterTool = function patchedUnregisterTool(name) {
|
|
170
|
+
const result = originalUnregister(name);
|
|
171
|
+
try { unwireOne(name); } catch {}
|
|
172
|
+
return result;
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
patched = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function unpatchModelContext() {
|
|
179
|
+
if (!patched) return;
|
|
180
|
+
if (originalRegister) mc.registerTool = originalRegister;
|
|
181
|
+
if (originalUnregister) mc.unregisterTool = originalUnregister;
|
|
182
|
+
patched = false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (autoRegisterExisting) {
|
|
186
|
+
for (const t of snapshotExisting(mc)) wireOne(t);
|
|
187
|
+
}
|
|
188
|
+
patchModelContext();
|
|
189
|
+
|
|
190
|
+
function stop() {
|
|
191
|
+
if (stopped) return;
|
|
192
|
+
stopped = true;
|
|
193
|
+
unpatchModelContext();
|
|
194
|
+
for (const webmcpName of [...wired.keys()]) unwireOne(webmcpName);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
stop,
|
|
199
|
+
/** Re-snapshot — useful if the host added tools through a different
|
|
200
|
+
* realm or before the polyfill installed itself. Idempotent: tools
|
|
201
|
+
* already mirrored are skipped. */
|
|
202
|
+
refresh() {
|
|
203
|
+
if (stopped) return;
|
|
204
|
+
for (const t of snapshotExisting(mc)) wireOne(t);
|
|
205
|
+
},
|
|
206
|
+
/** Names actually mirrored into the runtime (post-prefix). */
|
|
207
|
+
get wired() { return [...wired.values()]; },
|
|
208
|
+
/** Original WebMCP-side names (pre-prefix). */
|
|
209
|
+
get webmcpNames() { return [...wired.keys()]; },
|
|
210
|
+
};
|
|
211
|
+
}
|