@particle-academy/agent-integrations 0.33.0 → 0.35.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.
@@ -1,5 +1,5 @@
1
- import { ShareControls, AgentCursor, AgentActivityHighlight, AgentPanel } from './chunk-SJTLTITO.js';
2
- import { createSessionDescriptor, attachSseRelay } from './chunk-N6RLSLOF.js';
1
+ import { ShareControls, AgentCursor, AgentActivityHighlight, AgentPanel } from './chunk-ABP2THQV.js';
2
+ import { createSessionDescriptor, attachSseRelay } from './chunk-JDCB2NMM.js';
3
3
  import { attachInProcess } from './chunk-AFUULW5E.js';
4
4
  import { registerWhiteboardBridge } from './chunk-2U5JB6ZA.js';
5
5
  import './chunk-CT3LRNH7.js';
package/dist/index.cjs CHANGED
@@ -3896,10 +3896,196 @@ function registerFeaturesBridge(host, options) {
3896
3896
  }
3897
3897
  };
3898
3898
  }
3899
+
3900
+ // src/bridges/passkeys.ts
3899
3901
  var DEFAULT_AGENT14 = { id: "agent", name: "Agent", color: "#a855f7" };
3902
+ var str6 = (v, fallback = "") => typeof v === "string" ? v : fallback;
3903
+ function toSummary(record) {
3904
+ return {
3905
+ id: str6(record?.id),
3906
+ name: typeof record?.name === "string" ? record.name : null,
3907
+ createdAt: str6(record?.createdAt),
3908
+ lastUsedAt: typeof record?.lastUsedAt === "string" ? record.lastUsedAt : null,
3909
+ transports: Array.isArray(record?.transports) ? record.transports.map((t) => String(t)) : [],
3910
+ backedUp: record?.backedUp === true,
3911
+ aaguid: str6(record?.aaguid),
3912
+ clonedAt: typeof record?.clonedAt === "string" ? record.clonedAt : null
3913
+ };
3914
+ }
3915
+ var PASSKEY_BRIDGE_TOOLS = [
3916
+ "passkey_list",
3917
+ "passkey_status",
3918
+ "passkey_rename",
3919
+ "passkey_revoke",
3920
+ "passkey_begin_enrollment"
3921
+ ];
3922
+ function registerPasskeyBridge(host, options) {
3923
+ const { adapter } = options;
3924
+ const agent = { ...DEFAULT_AGENT14, ...options.agent ?? {} };
3925
+ const surfaceId = adapter.id ?? "passkeys";
3926
+ const disposers = [];
3927
+ ensureUndoToolsRegistered(host, { defaultAgentId: agent.id });
3928
+ const target = (elementId, label) => ({
3929
+ kind: "custom",
3930
+ screenId: adapter.screenId,
3931
+ elementId,
3932
+ label: label ?? surfaceId
3933
+ });
3934
+ const reg = (name, description, properties, required, handler, resolveTarget) => {
3935
+ const wrapped = async (args) => {
3936
+ try {
3937
+ return await handler(args);
3938
+ } catch (e) {
3939
+ return errorResult(e instanceof Error ? e.message : String(e));
3940
+ }
3941
+ };
3942
+ const final = resolveTarget ? wrapToolWithActivity(wrapped, {
3943
+ toolName: name,
3944
+ agent,
3945
+ kind: "custom",
3946
+ screenId: adapter.screenId,
3947
+ resolveTarget: ({ args }) => resolveTarget(args)
3948
+ }) : wrapped;
3949
+ disposers.push(
3950
+ host.registerTool(
3951
+ {
3952
+ name,
3953
+ description,
3954
+ inputSchema: {
3955
+ type: "object",
3956
+ properties,
3957
+ required,
3958
+ additionalProperties: false
3959
+ }
3960
+ },
3961
+ final
3962
+ )
3963
+ );
3964
+ };
3965
+ const findById = async (id) => (await adapter.list()).map(toSummary).find((p) => p.id === id) ?? null;
3966
+ reg(
3967
+ "passkey_list",
3968
+ "List the signed-in user's passkeys: credential id, label, when it was added and last used, transports, whether it is a synced passkey, and whether the server flagged it as a possible clone. Read-only. Public keys, user handles and signature counters are never returned.",
3969
+ {},
3970
+ [],
3971
+ async () => {
3972
+ const passkeys = (await adapter.list()).map(toSummary);
3973
+ return textResult(JSON.stringify(passkeys, null, 2), { passkeys, count: passkeys.length });
3974
+ },
3975
+ false
3976
+ );
3977
+ reg(
3978
+ "passkey_status",
3979
+ "Read what this browser can do (passkey support, a built-in authenticator, autofill sign-in) and what the management surface is currently showing (busy/error, a revoke awaiting confirmation, a rename in progress).",
3980
+ {},
3981
+ [],
3982
+ async () => {
3983
+ const support = adapter.support ? await adapter.support() : null;
3984
+ const state = adapter.state ? await adapter.state() : null;
3985
+ const out = {
3986
+ support,
3987
+ state,
3988
+ // Said in the payload, not only in the docs: an agent reading this to
3989
+ // work out "how do I sign the user in" should find the answer here.
3990
+ canCompleteCeremony: false,
3991
+ note: "A passkey ceremony needs a user gesture and a biometric or PIN. No tool on this bridge can perform one; ask the human to complete it on their device."
3992
+ };
3993
+ return textResult(JSON.stringify(out, null, 2), out);
3994
+ },
3995
+ false
3996
+ );
3997
+ reg(
3998
+ "passkey_rename",
3999
+ "Rename one passkey, by credential id (from passkey_list). Cosmetic and undoable with agent_undo.",
4000
+ {
4001
+ id: { type: "string", description: "base64url credential id." },
4002
+ name: { type: "string", description: "New label, e.g. 'MacBook Touch ID'. Empty string clears it." }
4003
+ },
4004
+ ["id", "name"],
4005
+ async (args) => {
4006
+ const id = str6(args.id);
4007
+ const name = str6(args.name);
4008
+ if (!id) return errorResult("id is required.");
4009
+ const existing = await findById(id);
4010
+ if (!existing) return errorResult(`No passkey with id ${id}.`);
4011
+ if (options.confirmRename) {
4012
+ if (!options.confirm) {
4013
+ return errorResult(
4014
+ "passkey_rename requires confirmation (confirmRename), but the host wired no confirm hook."
4015
+ );
4016
+ }
4017
+ const ok = await options.confirm({ action: "passkey_rename", id, name });
4018
+ if (!ok) return errorResult(`Declined: rename ${id} (human did not confirm).`);
4019
+ }
4020
+ const previous = existing.name ?? "";
4021
+ await adapter.rename({ id, name });
4022
+ fancyAutoCommon.pushUndoEntry(agent.id, {
4023
+ timestamp: Date.now(),
4024
+ bridgeId: surfaceId,
4025
+ action: "passkey_rename",
4026
+ label: `Renamed passkey to ${name || "(unnamed)"}`,
4027
+ undo: () => void adapter.rename({ id, name: previous }),
4028
+ redo: () => void adapter.rename({ id, name })
4029
+ });
4030
+ return textResult(`Renamed passkey ${id} to ${name || "(unnamed)"}`, { id, name, previous });
4031
+ },
4032
+ (args) => target(str6(args.id), `rename ${str6(args.id)}`)
4033
+ );
4034
+ reg(
4035
+ "passkey_revoke",
4036
+ "PROPOSE revoking one passkey. This does NOT revoke it: the request is staged on the surface and the human confirms or cancels there. Revoking the last passkey can lock the user out, and the response says when that is the case so you can tell them before they confirm.",
4037
+ { id: { type: "string", description: "base64url credential id." } },
4038
+ ["id"],
4039
+ async (args) => {
4040
+ const id = str6(args.id);
4041
+ if (!id) return errorResult("id is required.");
4042
+ const existing = await findById(id);
4043
+ if (!existing) return errorResult(`No passkey with id ${id}.`);
4044
+ const proposal = await adapter.proposeRevoke({ id });
4045
+ const isLastPasskey = proposal?.isLastPasskey === true;
4046
+ const out = {
4047
+ id,
4048
+ name: existing.name,
4049
+ staged: true,
4050
+ awaitingHuman: true,
4051
+ isLastPasskey
4052
+ };
4053
+ return textResult(
4054
+ isLastPasskey ? `Staged a revoke of ${existing.name ?? id}. This is the account's LAST passkey \u2014 confirming it removes passkey sign-in entirely. Waiting for the human to confirm or cancel.` : `Staged a revoke of ${existing.name ?? id}. Waiting for the human to confirm or cancel on the surface.`,
4055
+ out
4056
+ );
4057
+ },
4058
+ (args) => target(str6(args.id), `propose revoke ${str6(args.id)}`)
4059
+ );
4060
+ reg(
4061
+ "passkey_begin_enrollment",
4062
+ "Ask the surface to start enrolling a new passkey. This only opens the prompt: the ceremony completes on the human's authenticator (Touch ID, Windows Hello, phone, security key) or it does not complete at all. Returns immediately.",
4063
+ { name: { type: "string", description: "Optional label to pre-fill for the new passkey." } },
4064
+ [],
4065
+ async (args) => {
4066
+ if (!adapter.beginEnrollment) return errorResult("Host did not wire beginEnrollment().");
4067
+ const name = args.name === void 0 ? void 0 : str6(args.name);
4068
+ await adapter.beginEnrollment(name === void 0 ? {} : { name });
4069
+ const out = { awaitingHuman: true, ...name === void 0 ? {} : { name } };
4070
+ return textResult(
4071
+ "Enrollment prompt opened. The human has to complete it on their authenticator \u2014 there is no tool that can finish it for them.",
4072
+ out
4073
+ );
4074
+ },
4075
+ (args) => target(void 0, `begin enrollment${args.name ? ` (${str6(args.name)})` : ""}`)
4076
+ );
4077
+ return {
4078
+ id: `passkeys:${surfaceId}`,
4079
+ title: adapter.title ?? "Passkeys",
4080
+ dispose: () => {
4081
+ for (const d of disposers.splice(0)) d();
4082
+ }
4083
+ };
4084
+ }
4085
+ var DEFAULT_AGENT15 = { id: "agent", name: "Agent", color: "#a855f7" };
3900
4086
  function registerDocBridge(host, options) {
3901
4087
  const { adapter, surface } = options;
3902
- const agent = { ...DEFAULT_AGENT14, ...options.agent ?? {} };
4088
+ const agent = { ...DEFAULT_AGENT15, ...options.agent ?? {} };
3903
4089
  const reducer = options.reducer ?? fancyDocCommons.treeReducer();
3904
4090
  const stage = options.stagePolicy ?? fancyDocCommons.autoApply;
3905
4091
  const makeNode = options.makeNode ?? (({ id, type, parent, order }, props) => ({ id, type, parent, order, props }));
@@ -4271,6 +4457,7 @@ var SseRelayTransport = class {
4271
4457
  this.listeners = /* @__PURE__ */ new Set();
4272
4458
  this.state = "idle";
4273
4459
  this.peers = /* @__PURE__ */ new Set();
4460
+ this.peerListeners = /* @__PURE__ */ new Set();
4274
4461
  this.opts = options;
4275
4462
  this.expectedToken = options.token;
4276
4463
  }
@@ -4331,6 +4518,8 @@ var SseRelayTransport = class {
4331
4518
  this.es?.close();
4332
4519
  this.es = void 0;
4333
4520
  this.connected = false;
4521
+ this.peers.clear();
4522
+ this.notifyPeers();
4334
4523
  this.setState("closed");
4335
4524
  }
4336
4525
  onStateChange(listener) {
@@ -4338,6 +4527,28 @@ var SseRelayTransport = class {
4338
4527
  listener(this.state);
4339
4528
  return () => this.listeners.delete(listener);
4340
4529
  }
4530
+ /** How many remote peers (agents) are currently attached to this session. */
4531
+ peerCount() {
4532
+ return this.peers.size;
4533
+ }
4534
+ /**
4535
+ * Subscribe to peer arrivals/departures. Fires immediately with the current
4536
+ * count.
4537
+ *
4538
+ * This is the ONLY honest answer to "is an agent actually here?".
4539
+ * `onStateChange` reports the BROWSER's own channel to the relay, which opens
4540
+ * the instant sharing starts — so a UI keyed on it announces "Agent is
4541
+ * driving" to a human who has not yet handed the link to anybody, and can
4542
+ * never tell them when one really arrives.
4543
+ */
4544
+ onPeersChange(listener) {
4545
+ this.peerListeners.add(listener);
4546
+ listener(this.peers.size);
4547
+ return () => this.peerListeners.delete(listener);
4548
+ }
4549
+ notifyPeers() {
4550
+ for (const l of this.peerListeners) l(this.peers.size);
4551
+ }
4341
4552
  /**
4342
4553
  * For relays that wrap each frame with auth metadata: hosts can call this
4343
4554
  * directly when a frame arrives via a non-SSE path. The transport will
@@ -4372,6 +4583,7 @@ var SseRelayTransport = class {
4372
4583
  if ("method" in message && message.method === "notifications/peer_joined") {
4373
4584
  const subscriberId = String(message.params?.subscriberId ?? "peer");
4374
4585
  this.peers.add(subscriberId);
4586
+ this.notifyPeers();
4375
4587
  fancyAutoCommon.emitActivity({
4376
4588
  agentId: "agent",
4377
4589
  agentName: "Agent",
@@ -4386,6 +4598,7 @@ var SseRelayTransport = class {
4386
4598
  if ("method" in message && message.method === "notifications/peer_left") {
4387
4599
  const subscriberId = String(message.params?.subscriberId ?? "peer");
4388
4600
  this.peers.delete(subscriberId);
4601
+ this.notifyPeers();
4389
4602
  if (this.peers.size === 0) {
4390
4603
  fancyAutoCommon.emitActivity({
4391
4604
  agentId: "agent",
@@ -4431,11 +4644,11 @@ function attachSseRelay(server, options) {
4431
4644
 
4432
4645
  // src/sharing/use-co-browse-session.ts
4433
4646
  init_registry();
4434
- var DEFAULT_AGENT15 = { id: "agent", name: "Agent", color: "#a855f7" };
4647
+ var DEFAULT_AGENT16 = { id: "agent", name: "Agent", color: "#a855f7" };
4435
4648
  var USER = { id: "human", name: "You" };
4436
4649
  function useCoBrowseSession(options) {
4437
4650
  const { adapter, extraBridges } = options;
4438
- const agent = { ...DEFAULT_AGENT15, ...options.agent ?? {} };
4651
+ const agent = { ...DEFAULT_AGENT16, ...options.agent ?? {} };
4439
4652
  const relayBaseUrl = options.relayBaseUrl ?? "/agent-relay";
4440
4653
  const serverRef = react.useRef(null);
4441
4654
  const relayRef = react.useRef(null);
@@ -4443,6 +4656,7 @@ function useCoBrowseSession(options) {
4443
4656
  const disposeBridge = react.useRef(null);
4444
4657
  const [session, setSession] = react.useState(null);
4445
4658
  const [relayState, setRelayState] = react.useState("idle");
4659
+ const [agentCount, setAgentCount] = react.useState(0);
4446
4660
  react.useEffect(() => {
4447
4661
  const server = new MicroMcpServer({
4448
4662
  info: options.info ?? { name: "fancy-co-browse", version: "0.1.0" },
@@ -4477,6 +4691,7 @@ function useCoBrowseSession(options) {
4477
4691
  }
4478
4692
  const relay = attachSseRelay(server, { baseUrl: relayBaseUrl, sessionId: descriptor.id, token: descriptor.token });
4479
4693
  relay.onStateChange(setRelayState);
4694
+ relay.onPeersChange(setAgentCount);
4480
4695
  relayRef.current = relay;
4481
4696
  setSession(descriptor);
4482
4697
  }, [relayBaseUrl, options]);
@@ -4485,6 +4700,7 @@ function useCoBrowseSession(options) {
4485
4700
  relayRef.current?.close();
4486
4701
  relayRef.current = null;
4487
4702
  setRelayState("idle");
4703
+ setAgentCount(0);
4488
4704
  setSession(null);
4489
4705
  if (current) {
4490
4706
  const csrf = options.csrfToken?.() ?? "";
@@ -4507,7 +4723,16 @@ function useCoBrowseSession(options) {
4507
4723
  meta: event
4508
4724
  });
4509
4725
  }, []);
4510
- return { server: serverRef.current, session, relayState, startShare, stopShare, observeUser };
4726
+ return {
4727
+ server: serverRef.current,
4728
+ session,
4729
+ relayState,
4730
+ agentConnected: agentCount > 0,
4731
+ agentCount,
4732
+ startShare,
4733
+ stopShare,
4734
+ observeUser
4735
+ };
4511
4736
  }
4512
4737
  function AgentPanel({ agent, activity, onSubmit, busy, actions, className, style }) {
4513
4738
  const scrollRef = react.useRef(null);
@@ -5230,10 +5455,12 @@ function ManualPopover({
5230
5455
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "fai-connect__copy-btn", onClick: onCopy, children: copied ? "Copied" : "Copy snippet" })
5231
5456
  ] });
5232
5457
  }
5458
+ var LIFECYCLE_ACTIONS = /* @__PURE__ */ new Set(["agent_connected", "agent_disconnected"]);
5233
5459
  function CoBrowsePresence({ session, connectUrl, shareBaseUrl, className }) {
5234
5460
  const { events } = useAgentActivity(void 0, { capacity: 40 });
5235
- const lastAgentAction = [...events].reverse().find((e) => (e.source ?? "agent") !== "user");
5236
- const connected = session.relayState === "open";
5461
+ const lastAgentAction = [...events].reverse().find((e) => (e.source ?? "agent") !== "user" && !LIFECYCLE_ACTIONS.has(e.action));
5462
+ const connected = session.agentConnected;
5463
+ const activity = react.useMemo(() => events.map(toPanelActivity), [events]);
5237
5464
  if (!session.session) {
5238
5465
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className, "data-co-browse-presence": "idle", children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void session.startShare(), "data-co-browse-start": true, children: "Let an agent drive" }) });
5239
5466
  }
@@ -5251,12 +5478,25 @@ function CoBrowsePresence({ session, connectUrl, shareBaseUrl, className }) {
5251
5478
  onStart: () => void session.startShare(),
5252
5479
  onStop: session.stopShare,
5253
5480
  status: session.relayState,
5481
+ agentConnected: connected,
5482
+ activity,
5254
5483
  shareBaseUrl
5255
5484
  }
5256
5485
  ),
5257
5486
  connectUrl && /* @__PURE__ */ jsxRuntime.jsx(ConnectorButtons, { serverName: "Fancy UI co-browse", mcpUrl: connectUrl })
5258
5487
  ] });
5259
5488
  }
5489
+ function toPanelActivity(event) {
5490
+ const who = (event.source ?? "agent") === "user" ? "You" : event.agentName ?? "Agent";
5491
+ return {
5492
+ id: `${event.timestamp}-${event.action}-${event.target?.elementId ?? event.target?.label ?? ""}`,
5493
+ at: event.timestamp,
5494
+ kind: LIFECYCLE_ACTIONS.has(event.action) ? "info" : "tool",
5495
+ source: who,
5496
+ text: event.target?.label ?? event.action,
5497
+ detail: event.meta
5498
+ };
5499
+ }
5260
5500
  CoBrowsePresence.displayName = "CoBrowsePresence";
5261
5501
  var CURSOR_EDGE_INSET = 24;
5262
5502
  var FLUID_CURSOR_SPEED_PX_PER_SECOND = 1050;
@@ -5275,19 +5515,26 @@ function fluidGlideDuration(from, to) {
5275
5515
  const distance = Math.hypot(to.x - from.x, to.y - from.y);
5276
5516
  return Math.round(Math.min(MAX_GLIDE_MS, Math.max(MIN_GLIDE_MS, distance / FLUID_CURSOR_SPEED_PX_PER_SECOND * 1e3)));
5277
5517
  }
5278
- function CoBrowseCursorLayer({ active = true, zIndex = 2147483e3 }) {
5518
+ var LIFECYCLE_ACTIONS2 = /* @__PURE__ */ new Set(["agent_connected", "agent_disconnected"]);
5519
+ function CoBrowseCursorLayer({
5520
+ active = true,
5521
+ zIndex = 2147483e3,
5522
+ idleAfterMs = 15e3
5523
+ }) {
5279
5524
  const { latest } = useAgentActivity(void 0, { capacity: 12 });
5280
5525
  const [cursor, setCursor] = react.useState(
5281
5526
  null
5282
5527
  );
5283
5528
  const [pulse, setPulse] = react.useState(null);
5529
+ const [lastActionAt, setLastActionAt] = react.useState(0);
5284
5530
  react.useEffect(() => {
5285
5531
  if (!latest || (latest.source ?? "agent") === "user") return;
5286
- if (latest.action === "agent_disconnected") {
5287
- setCursor((prev) => prev ? { ...prev, status: "Standing by", glideMs: 0 } : prev);
5532
+ if (LIFECYCLE_ACTIONS2.has(latest.action)) {
5533
+ setCursor((prev) => prev ? { ...prev, status: "Standing by", glideMs: 0 } : null);
5288
5534
  setPulse(null);
5289
5535
  return;
5290
5536
  }
5537
+ setLastActionAt(latest.timestamp);
5291
5538
  const color = latest.agentColor ?? "#a855f7";
5292
5539
  const name = latest.agentName ?? "Agent";
5293
5540
  const status = latest.target?.label ?? latest.action;
@@ -5309,6 +5556,14 @@ function CoBrowseCursorLayer({ active = true, zIndex = 2147483e3 }) {
5309
5556
  );
5310
5557
  }
5311
5558
  }, [latest?.timestamp]);
5559
+ react.useEffect(() => {
5560
+ if (!idleAfterMs || !lastActionAt) return;
5561
+ const timer = setTimeout(() => {
5562
+ setCursor(null);
5563
+ setPulse(null);
5564
+ }, idleAfterMs);
5565
+ return () => clearTimeout(timer);
5566
+ }, [lastActionAt, idleAfterMs]);
5312
5567
  if (!active || !cursor || typeof document === "undefined") return null;
5313
5568
  return reactDom.createPortal(
5314
5569
  /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-co-browse-cursor-layer": "", style: { position: "fixed", inset: 0, pointerEvents: "none", zIndex }, children: [
@@ -5654,6 +5909,7 @@ exports.MCPB_MIN_NODE = MCPB_MIN_NODE;
5654
5909
  exports.MCP_PROTOCOL_VERSION = MCP_PROTOCOL_VERSION;
5655
5910
  exports.MemoryHumanPlusEventStore = MemoryHumanPlusEventStore;
5656
5911
  exports.MicroMcpServer = MicroMcpServer;
5912
+ exports.PASSKEY_BRIDGE_TOOLS = PASSKEY_BRIDGE_TOOLS;
5657
5913
  exports.RelayTransport = RelayTransport;
5658
5914
  exports.ScreensActivityBridge = ScreensActivityBridge;
5659
5915
  exports.ShareControls = ShareControls;
@@ -5695,6 +5951,7 @@ exports.registerFormBridge = registerFormBridge;
5695
5951
  exports.registerGitBridge = registerGitBridge;
5696
5952
  exports.registerMapBridge = registerMapBridge;
5697
5953
  exports.registerNavigationBridge = registerNavigationBridge;
5954
+ exports.registerPasskeyBridge = registerPasskeyBridge;
5698
5955
  exports.registerSceneBridge = registerSceneBridge;
5699
5956
  exports.registerScreensBridge = registerScreensBridge;
5700
5957
  exports.registerSheetsBridge = registerSheetsBridge;