@matterfact/embed 0.9.0 → 0.11.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.
Files changed (43) hide show
  1. package/README.md +122 -0
  2. package/dist/{chunk-6EM7T2JV.js → chunk-AANODHBV.js} +217 -15
  3. package/dist/chunk-AANODHBV.js.map +1 -0
  4. package/dist/{chunk-URGQBG4I.js → chunk-CTZEDOH7.js} +218 -17
  5. package/dist/chunk-CTZEDOH7.js.map +1 -0
  6. package/dist/{chunk-BKKXHSYU.js → chunk-JO3GWFMJ.js} +2 -2
  7. package/dist/chunk-SKJFF7RD.js +2 -0
  8. package/dist/chunk-Y7I25VHL.js +3 -0
  9. package/dist/chunk-Y7I25VHL.js.map +7 -0
  10. package/dist/context-FR7VFENN.js +3 -0
  11. package/dist/{context-ACFBWIFH.js.map → context-FR7VFENN.js.map} +1 -1
  12. package/dist/{context-U2HJJN2S.js → context-IK5MECUW.js} +4 -2
  13. package/dist/embed.js +1 -1
  14. package/dist/embed.js.map +3 -3
  15. package/dist/index.cjs +247 -13
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +88 -1
  18. package/dist/index.d.ts +88 -1
  19. package/dist/index.js +25 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/react.cjs +302 -15
  22. package/dist/react.cjs.map +1 -1
  23. package/dist/react.d.cts +174 -2
  24. package/dist/react.d.ts +174 -2
  25. package/dist/react.js +79 -3
  26. package/dist/react.js.map +1 -1
  27. package/dist/{snapshot-4GXT6PKZ.js → snapshot-2V5SDSH2.js} +2 -2
  28. package/dist/{snapshot-Y75SCGCM.js → snapshot-Y5BF2UJR.js} +3 -3
  29. package/dist/{snapshot-Y75SCGCM.js.map → snapshot-Y5BF2UJR.js.map} +1 -1
  30. package/examples/embed-demo/README.md +22 -0
  31. package/examples/embed-demo/src/App.tsx +105 -2
  32. package/examples/embed-demo/src/styles.css +29 -0
  33. package/package.json +1 -1
  34. package/dist/chunk-6EM7T2JV.js.map +0 -1
  35. package/dist/chunk-FEXG4LQJ.js +0 -3
  36. package/dist/chunk-FEXG4LQJ.js.map +0 -7
  37. package/dist/chunk-NWNMS34P.js +0 -2
  38. package/dist/chunk-URGQBG4I.js.map +0 -1
  39. package/dist/context-ACFBWIFH.js +0 -3
  40. /package/dist/{chunk-BKKXHSYU.js.map → chunk-JO3GWFMJ.js.map} +0 -0
  41. /package/dist/{chunk-NWNMS34P.js.map → chunk-SKJFF7RD.js.map} +0 -0
  42. /package/dist/{context-U2HJJN2S.js.map → context-IK5MECUW.js.map} +0 -0
  43. /package/dist/{snapshot-4GXT6PKZ.js.map → snapshot-2V5SDSH2.js.map} +0 -0
package/README.md CHANGED
@@ -164,6 +164,60 @@ in the widget (authorized by the signed-in user's own matterfact access — no t
164
164
  optional **Open on page** deeplink back to your route. matterfact resolves the document from
165
165
  its own systems by `MF_DOC_ID`; it never reads your database.
166
166
 
167
+ ### Content the user _isn't_ looking at
168
+
169
+ The site map tells the agent a route _carries_ matterfact content; the two props below let it
170
+ actually **reach** that content from anywhere, without the user navigating there first.
171
+
172
+ **Boards on any route — `artifacts`.** Declare your matterfact artifacts once and the agent
173
+ can open any of them from any page, even routes where you don't embed the `<MatterfactArtifact>`
174
+ iframe:
175
+
176
+ ```tsx
177
+ <MatterfactAgent
178
+ artifacts={[
179
+ { slug: 'exec-departure-tracker', owner: 'you@firm.com', token: '…', label: 'Talent Bank' },
180
+ { slug: 'global-datacenter-buildout', owner: 'you@firm.com', token: '…', label: 'Data Centers' },
181
+ ]}
182
+ />
183
+ ```
184
+
185
+ ```js
186
+ // script tag — same shape
187
+ window.matterfact = { artifacts: [{ slug: '…', owner: '…', token: '…', label: '…' }] };
188
+ ```
189
+
190
+ This replaces the old workaround of mounting hidden, zero-size `<MatterfactArtifact>` iframes
191
+ just to make a board's grant reachable. The `token` stays in this host-page config and in the
192
+ grant channel — it never enters the chat transcript or reaches the model.
193
+
194
+ **A resolver for content you look up per-request — `resolve`.** When your set of documents or
195
+ boards is per-user or per-ticker (too large to declare), give the agent a function that turns a
196
+ classification into a concrete reference. matterfact defines the tool contract (name, arguments,
197
+ result shape, and how the agent uses it — so it costs you no agent-instruction budget); you
198
+ supply the implementation:
199
+
200
+ ```tsx
201
+ <MatterfactAgent
202
+ resolve={{
203
+ // doctype + a key (e.g. a ticker) -> the matterfact document id, from YOUR lookup.
204
+ document: async ({ doctype, key }) => {
205
+ const row = await myHub.lookup(doctype, key);
206
+ return { id: row.MF_DOC_ID, label: `${key} ${doctype}`, href: hrefFor(doctype, key) };
207
+ },
208
+ // a slug -> a read grant, when you resolve boards dynamically instead of declaring them.
209
+ artifact: async ({ slug }) => myHub.artifactGrant(slug),
210
+ }}
211
+ />
212
+ ```
213
+
214
+ The agent calls `mf.resolveDocument` / `mf.resolveArtifact` when the site map shows the content
215
+ lives on another route, gets back an id (and, for a document, an optional `href` deeplink), and
216
+ cites it as a chip — exactly as for on-page content. **matterfact never queries your data**: your
217
+ function runs the lookup, matterfact only ever receives the id it returns. `resolve` is a host
218
+ tool (see [Host tools](#host-tools)), so your app's `host_tools` policy must allow `mf.*` — it
219
+ does by default.
220
+
167
221
  ## Signing users in
168
222
 
169
223
  By default the widget runs its own sign-in in a popup (the only way an embedded frame can
@@ -382,6 +436,74 @@ Everything else about page observation still applies here: values are redacted b
382
436
  leaves the page, row counts are capped, and `pageContext={false}` turns this off along with
383
437
  everything else (see above).
384
438
 
439
+ ## Host tools
440
+
441
+ `hoist.navigate` above is one instance of a general mechanism: **the host page can offer the
442
+ agent tools to call**. There are three kinds, distinguished by who owns the contract and who
443
+ runs the code:
444
+
445
+ | Prefix | Contract | Runs where | Examples |
446
+ | ---------- | ---------- | ------------------- | ------------------------------------------------ |
447
+ | `hoist.*` | matterfact | the widget's adapter | `hoist.navigate` (above) |
448
+ | `mf.*` | matterfact | **your** function | `mf.resolveDocument`, `mf.resolveArtifact` (above) |
449
+ | `app.*` | **you** | **your** function | anything you declare |
450
+
451
+ `hoist.*` and `mf.*` you get by turning on the features above. `app.*` is your own tools, which
452
+ you declare with the `tools` prop:
453
+
454
+ ```tsx
455
+ <MatterfactAgent
456
+ tools={[
457
+ {
458
+ name: 'exportBook', // advertised to the agent as `app.exportBook`
459
+ description: 'Export the current book to CSV.',
460
+ inputSchema: { type: 'object', properties: { scope: { type: 'string' } } },
461
+ confirm: 'required', // 'required' (default) shows an approval card; 'auto' runs without asking
462
+ handler: async ({ scope }) => myApp.exportBook(scope),
463
+ },
464
+ ]}
465
+ />
466
+ ```
467
+
468
+ ```js
469
+ // script tag — same shape
470
+ window.matterfact = { tools: [{ name: '…', description: '…', handler: async () => {} }] };
471
+ ```
472
+
473
+ Names are namespaced into `app.` automatically, so a host tool can never collide with a
474
+ matterfact one. A tool's `handler` result and any error it throws are **redacted before leaving
475
+ the page**; a handler that throws, rejects, or hangs (beyond 30s) becomes a normal "not
476
+ completed" result the agent reads and adapts to — never a crash. Like `actions`, changing the
477
+ _shape_ of your tools (a name, description, schema, or confirm policy) remounts the widget so the
478
+ new set is advertised; changing only a handler's body does not, so an inline `handler` closure
479
+ won't churn the chat session.
480
+
481
+ ### Who decides which tools are exposed
482
+
483
+ Advertising a tool is necessary but not sufficient: **which tools an app may actually expose is
484
+ a server-side setting** on your embed app (`host_tools`: `off` / `allowlist` / `all`, plus an
485
+ allow-list of names). The default allows matterfact's own `hoist.*` and `mf.*` and denies
486
+ arbitrary `app.*` until an operator grants it — so `resolve` works out of the box, and turning on
487
+ your first `app.*` tool is a deliberate decision made in the admin console, not something a page
488
+ can do on its own. Ask your matterfact contact to enable the `app.*` names you need.
489
+
490
+ ### Telemetry — `onEvent`
491
+
492
+ One hook pipes the whole widget into your own observability — lifecycle (`ready`, `open`,
493
+ `close`, `navigate`, `auth`, `error`), each chat turn (`chat`), and every tool call (`tool`):
494
+
495
+ ```tsx
496
+ <MatterfactAgent
497
+ onEvent={(e) => myTelemetry.track(`mf.${e.type}`, e)}
498
+ />
499
+ ```
500
+
501
+ Fire-and-forget and fully isolated — a throw or a slow callback here never blocks or breaks the
502
+ widget, a tool call, or a run. Chat events are content-free (phase + opaque chat id, never
503
+ message text). `onToolEvent` still works as a tool-only subset for back-compat.
504
+
505
+ **→ Full event reference: [`docs/telemetry.md`](docs/telemetry.md).**
506
+
385
507
  ## Microphone / dictation
386
508
 
387
509
  The composer supports voice dictation; the loader grants the iframe `allow="microphone"`.
@@ -376,6 +376,157 @@ function executeHostTool(call, win = typeof window !== "undefined" ? window : vo
376
376
  }
377
377
  }
378
378
 
379
+ // src/adapters/registry.ts
380
+ var MAX_TOOLS = 16;
381
+ var HOST_HANDLER_TIMEOUT_MS = 3e4;
382
+ function mfGlobal(win) {
383
+ return win?.matterfact ?? {};
384
+ }
385
+ function toolClass(name) {
386
+ if (name.startsWith("hoist.")) return "hoist";
387
+ if (name.startsWith("mf.")) return "mf";
388
+ return "app";
389
+ }
390
+ function emitEvent(win, e) {
391
+ const cb = mfGlobal(win).onEvent;
392
+ if (typeof cb !== "function") return;
393
+ try {
394
+ cb(e);
395
+ } catch {
396
+ }
397
+ }
398
+ function emitToolEvent(win, e) {
399
+ emitEvent(win, { type: "tool", ...e });
400
+ const cb = mfGlobal(win).onToolEvent;
401
+ if (typeof cb !== "function") return;
402
+ try {
403
+ cb(e);
404
+ } catch {
405
+ }
406
+ }
407
+ function mfTools(win) {
408
+ const { document: document2, artifact } = mfGlobal(win).resolve ?? {};
409
+ const out = [];
410
+ if (typeof document2 === "function") {
411
+ out.push({
412
+ name: "mf.resolveDocument",
413
+ description: 'Resolve a matterfact document (dossier, report, briefing) that is NOT on the current page to a concrete id. Call it with the document type and a key (e.g. a ticker) when the site map shows the document lives on another route. Returns {id, label, href}. Cite the result inline as <MFRef kind="document" id="<id>" label="<label>" /> \u2014 it renders as a chip that opens the document in the side panel; href is its "open on page" deeplink.',
414
+ inputSchema: {
415
+ type: "object",
416
+ properties: {
417
+ doctype: { type: "string" },
418
+ key: { type: "string" }
419
+ },
420
+ required: ["doctype", "key"]
421
+ },
422
+ readOnly: true,
423
+ confirm: "auto"
424
+ });
425
+ }
426
+ if (typeof artifact === "function") {
427
+ out.push({
428
+ name: "mf.resolveArtifact",
429
+ description: 'Resolve a matterfact artifact (an interactive board) that is NOT co-embedded on the current page to a read grant. Call it with the artifact slug. Returns {id, owner, token, label}; the grant is applied for you. Cite the result inline as <MFRef kind="artifact" id="<id>" label="<label>" /> \u2014 it renders as a chip that opens the artifact in the side panel.',
430
+ inputSchema: {
431
+ type: "object",
432
+ properties: { slug: { type: "string" } },
433
+ required: ["slug"]
434
+ },
435
+ readOnly: true,
436
+ confirm: "auto"
437
+ });
438
+ }
439
+ return out;
440
+ }
441
+ var APP_NAME_RE = /^[A-Za-z0-9._-]+$/;
442
+ function appName(name) {
443
+ return name.startsWith("app.") ? name : `app.${name}`;
444
+ }
445
+ function appTools(win) {
446
+ const defs = mfGlobal(win).tools;
447
+ if (!Array.isArray(defs)) return [];
448
+ const out = [];
449
+ for (const d of defs) {
450
+ const raw = (d?.name ?? "").trim();
451
+ if (!raw || typeof d.handler !== "function") continue;
452
+ const name = appName(raw);
453
+ if (!APP_NAME_RE.test(name)) continue;
454
+ out.push({
455
+ name,
456
+ description: String(d.description ?? `Run the host tool ${name}.`),
457
+ inputSchema: d.inputSchema ?? { type: "object", properties: {} },
458
+ readOnly: false,
459
+ confirm: d.confirm === "auto" ? "auto" : "required"
460
+ });
461
+ }
462
+ return out;
463
+ }
464
+ function advertise(win = typeof window !== "undefined" ? window : void 0) {
465
+ const merged = [];
466
+ const seen = /* @__PURE__ */ new Set();
467
+ const push = (tools) => {
468
+ for (const t of tools) {
469
+ if (seen.has(t.name)) continue;
470
+ seen.add(t.name);
471
+ merged.push(t);
472
+ }
473
+ };
474
+ try {
475
+ push(advertiseTools(win));
476
+ } catch {
477
+ }
478
+ try {
479
+ push(mfTools(win));
480
+ push(appTools(win));
481
+ } catch {
482
+ }
483
+ return merged.slice(0, MAX_TOOLS);
484
+ }
485
+ function timeout(ms) {
486
+ return new Promise(
487
+ (_, reject) => setTimeout(() => reject(new Error("host tool timed out")), ms)
488
+ );
489
+ }
490
+ function redactResult(value) {
491
+ if (value === void 0 || value === null) return value;
492
+ if (typeof value === "string") return redact(value);
493
+ try {
494
+ return JSON.parse(redact(JSON.stringify(value)));
495
+ } catch {
496
+ return redact(String(value));
497
+ }
498
+ }
499
+ async function execute(call, win = typeof window !== "undefined" ? window : void 0) {
500
+ const cls = toolClass(call.name);
501
+ try {
502
+ if (cls === "hoist") {
503
+ return executeHostTool(call, win);
504
+ }
505
+ const handler = resolveHandler(win, call.name);
506
+ if (!handler) return { ok: false, error: "unknown tool" };
507
+ const result = await Promise.race([
508
+ Promise.resolve(handler(call.args ?? {})),
509
+ timeout(HOST_HANDLER_TIMEOUT_MS)
510
+ ]);
511
+ return { ok: true, result: redactResult(result) };
512
+ } catch (e) {
513
+ return {
514
+ ok: false,
515
+ error: redact(e instanceof Error ? e.message : "host tool failed")
516
+ };
517
+ }
518
+ }
519
+ function resolveHandler(win, name) {
520
+ const mf = mfGlobal(win);
521
+ if (name === "mf.resolveDocument") return mf.resolve?.document;
522
+ if (name === "mf.resolveArtifact") return mf.resolve?.artifact;
523
+ if (name.startsWith("app.")) {
524
+ const def = (mf.tools ?? []).find((d) => appName((d?.name ?? "").trim()) === name);
525
+ return def?.handler;
526
+ }
527
+ return void 0;
528
+ }
529
+
379
530
  // src/context.ts
380
531
  var widgetOrigin = "";
381
532
  var MAX_ACTIVITY = 40;
@@ -503,12 +654,31 @@ function grantsFromIframeSrcs(srcs, origin) {
503
654
  }
504
655
  return out;
505
656
  }
657
+ function readDeclaredGrants() {
658
+ const declared = window.matterfact?.artifacts;
659
+ if (!Array.isArray(declared)) return [];
660
+ const out = [];
661
+ for (const a of declared) {
662
+ const id = (a?.slug || "").trim();
663
+ const owner = (a?.owner || "").trim();
664
+ const token = (a?.token || "").trim();
665
+ if (!id || !owner || !token) continue;
666
+ if (out.some((g) => g.id === id)) continue;
667
+ out.push({ id, owner, token });
668
+ }
669
+ return out;
670
+ }
506
671
  function readArtifactGrants() {
507
672
  if (!widgetOrigin) return [];
508
673
  const srcs = Array.from(document.querySelectorAll("iframe")).map(
509
674
  (f) => f.getAttribute("src") || ""
510
675
  );
511
- return grantsFromIframeSrcs(srcs, widgetOrigin);
676
+ const scanned = grantsFromIframeSrcs(srcs, widgetOrigin);
677
+ const out = [...scanned];
678
+ for (const g of readDeclaredGrants()) {
679
+ if (!out.some((s) => s.id === g.id)) out.push(g);
680
+ }
681
+ return out;
512
682
  }
513
683
  function publishArtifactGrants() {
514
684
  send?.({ type: "host.artifactGrants", grants: readArtifactGrants() });
@@ -599,10 +769,7 @@ function watchNavigation() {
599
769
  void publishContext();
600
770
  publishArtifactGrants();
601
771
  publishSitemap();
602
- send?.({
603
- type: "host.tools",
604
- tools: advertiseTools(typeof window !== "undefined" ? window : void 0)
605
- });
772
+ publishTools();
606
773
  };
607
774
  navFire = fire;
608
775
  for (const name of ["pushState", "replaceState"]) {
@@ -646,7 +813,7 @@ function describeControl(el) {
646
813
  return describe(el);
647
814
  }
648
815
  async function loadSnapshotModule() {
649
- const m = await import("./snapshot-4GXT6PKZ.js");
816
+ const m = await import("./snapshot-2V5SDSH2.js");
650
817
  snapshotModule = m;
651
818
  return m;
652
819
  }
@@ -710,10 +877,24 @@ async function callTool(call, emit) {
710
877
  });
711
878
  return;
712
879
  }
713
- const r = executeHostTool(
714
- call,
715
- typeof window !== "undefined" ? window : void 0
716
- );
880
+ const win = typeof window !== "undefined" ? window : void 0;
881
+ const cls = toolClass(call.name);
882
+ const startedAt = Date.now();
883
+ emitToolEvent(win, {
884
+ phase: "call",
885
+ name: call.name,
886
+ toolClass: cls,
887
+ args: call.args
888
+ });
889
+ const r = await execute(call, win);
890
+ emitToolEvent(win, {
891
+ phase: "result",
892
+ name: call.name,
893
+ toolClass: cls,
894
+ ok: r.ok,
895
+ error: r.error,
896
+ ms: Date.now() - startedAt
897
+ });
717
898
  emit({
718
899
  type: "host.toolResult",
719
900
  callId: call.callId,
@@ -791,10 +972,30 @@ function start(emit, origin, pageContext = true, provider) {
791
972
  const theme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
792
973
  emit({ type: "host.theme", mode: theme });
793
974
  if (pageContextOn) {
794
- const tools = advertiseTools(
795
- typeof window !== "undefined" ? window : void 0
796
- );
797
- if (tools.length) emit({ type: "host.tools", tools });
975
+ const w = typeof window !== "undefined" ? window : void 0;
976
+ const tools = advertise(w);
977
+ if (tools.length) {
978
+ emit({ type: "host.tools", tools });
979
+ for (const t of tools) {
980
+ emitToolEvent(w, {
981
+ phase: "advertise",
982
+ name: t.name,
983
+ toolClass: toolClass(t.name)
984
+ });
985
+ }
986
+ }
987
+ }
988
+ }
989
+ function publishTools() {
990
+ const win = typeof window !== "undefined" ? window : void 0;
991
+ const tools = advertise(win);
992
+ send?.({ type: "host.tools", tools });
993
+ for (const t of tools) {
994
+ emitToolEvent(win, {
995
+ phase: "advertise",
996
+ name: t.name,
997
+ toolClass: toolClass(t.name)
998
+ });
798
999
  }
799
1000
  }
800
1001
 
@@ -806,6 +1007,7 @@ export {
806
1007
  readPageContext,
807
1008
  provideContext,
808
1009
  grantsFromIframeSrcs,
1010
+ readDeclaredGrants,
809
1011
  navigateHost,
810
1012
  sendSnapshot,
811
1013
  sendRegion,
@@ -813,4 +1015,4 @@ export {
813
1015
  stop,
814
1016
  start
815
1017
  };
816
- //# sourceMappingURL=chunk-6EM7T2JV.js.map
1018
+ //# sourceMappingURL=chunk-AANODHBV.js.map