@nevescloud/pip 3.2.1 → 3.4.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/package.json +4 -1
- package/pip-core.esm.js +16 -1
- 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.0",
|
|
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
|
@@ -258,6 +258,19 @@ const CSS = `
|
|
|
258
258
|
.pip-scroll::-webkit-scrollbar { width: 6px; }
|
|
259
259
|
.pip-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
260
260
|
.pip-scroll::-webkit-scrollbar-thumb { background: var(--pip-border, light-dark(rgba(0,0,0,0.10), rgba(255,255,255,0.12))); border-radius: 3px; }
|
|
261
|
+
/* Backdrop dim while the slash autocomplete is open — Apple-style focus
|
|
262
|
+
shift (Spotlight, Quick Look). History stays glanceable but visually
|
|
263
|
+
recedes so the menu is the active surface. */
|
|
264
|
+
.pip-scroll {
|
|
265
|
+
transition: opacity 140ms ease-out;
|
|
266
|
+
}
|
|
267
|
+
.pip-scroll.is-backdrop {
|
|
268
|
+
opacity: 0.28;
|
|
269
|
+
pointer-events: none;
|
|
270
|
+
}
|
|
271
|
+
@media (prefers-reduced-motion: reduce) {
|
|
272
|
+
.pip-scroll { transition: none; }
|
|
273
|
+
}
|
|
261
274
|
|
|
262
275
|
.pip-notify {
|
|
263
276
|
font-size: var(--pip-t-caption, 12px);
|
|
@@ -2024,7 +2037,8 @@ export function createPip(opts = {}) {
|
|
|
2024
2037
|
|
|
2025
2038
|
function renderSlashList() {
|
|
2026
2039
|
slashList.innerHTML = "";
|
|
2027
|
-
if (!slashCurrent.length) { slashList.hidden = true; return; }
|
|
2040
|
+
if (!slashCurrent.length) { slashList.hidden = true; scroll.classList.remove("is-backdrop"); return; }
|
|
2041
|
+
scroll.classList.add("is-backdrop");
|
|
2028
2042
|
slashCurrent.forEach((item, i) => {
|
|
2029
2043
|
const li = document.createElement("li");
|
|
2030
2044
|
li.setAttribute("role", "option");
|
|
@@ -2065,6 +2079,7 @@ export function createPip(opts = {}) {
|
|
|
2065
2079
|
slashCmdContext = null;
|
|
2066
2080
|
slashList.hidden = true;
|
|
2067
2081
|
slashList.innerHTML = "";
|
|
2082
|
+
scroll.classList.remove("is-backdrop");
|
|
2068
2083
|
}
|
|
2069
2084
|
|
|
2070
2085
|
function updateSlashSuggest() {
|
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
|
+
}
|