@particle-academy/agent-integrations 0.4.0 → 0.6.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/README.md +45 -0
- package/dist/bridges-flow.js +340 -3
- package/dist/bridges-flow.js.map +1 -1
- package/dist/{chunk-E4AICMFZ.js → chunk-5XELJIJR.js} +3 -3
- package/dist/chunk-5XELJIJR.js.map +1 -0
- package/dist/{chunk-6LTKCNLF.js → chunk-AFUULW5E.js} +3 -34
- package/dist/chunk-AFUULW5E.js.map +1 -0
- package/dist/chunk-G6N2TQVO.js +34 -0
- package/dist/chunk-G6N2TQVO.js.map +1 -0
- package/dist/chunk-IJ6JX5VC.js +3 -0
- package/dist/chunk-IJ6JX5VC.js.map +1 -0
- package/dist/{chunk-JMYPUAFH.js → chunk-LVQXIUJH.js} +2 -2
- package/dist/{chunk-JMYPUAFH.js.map → chunk-LVQXIUJH.js.map} +1 -1
- package/dist/chunk-OIX2ANFS.js +386 -0
- package/dist/chunk-OIX2ANFS.js.map +1 -0
- package/dist/chunk-ZHAK2DQR.js +289 -0
- package/dist/chunk-ZHAK2DQR.js.map +1 -0
- package/dist/components/SharedWhiteboard/index.d.cts +55 -0
- package/dist/components/SharedWhiteboard/index.d.ts +55 -0
- package/dist/components-shared-whiteboard.cjs +1533 -0
- package/dist/components-shared-whiteboard.cjs.map +1 -0
- package/dist/components-shared-whiteboard.js +285 -0
- package/dist/components-shared-whiteboard.js.map +1 -0
- package/dist/index.cjs +249 -1287
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -55
- package/dist/index.d.ts +4 -55
- package/dist/index.js +9 -563
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +2 -1
- package/dist/relay-server/index.d.cts +134 -0
- package/dist/relay-server/index.d.ts +134 -0
- package/dist/relay-server-cli.cjs +483 -0
- package/dist/relay-server-cli.cjs.map +1 -0
- package/dist/relay-server-cli.js +98 -0
- package/dist/relay-server-cli.js.map +1 -0
- package/dist/relay-server.cjs +389 -0
- package/dist/relay-server.cjs.map +1 -0
- package/dist/relay-server.js +3 -0
- package/dist/relay-server.js.map +1 -0
- package/dist/sharing/index.d.cts +2 -34
- package/dist/sharing/index.d.ts +2 -34
- package/dist/sharing.js +2 -1
- package/dist/sheets-adapter.cjs +1 -1
- package/dist/sheets-adapter.cjs.map +1 -1
- package/dist/sheets-adapter.d.cts +11 -7
- package/dist/sheets-adapter.d.ts +11 -7
- package/dist/sheets-adapter.js +1 -1
- package/dist/token-CrJF76oH.d.cts +34 -0
- package/dist/token-CrJF76oH.d.ts +34 -0
- package/docs/relay-server.md +126 -0
- package/package.json +66 -7
- package/dist/chunk-6LTKCNLF.js.map +0 -1
- package/dist/chunk-E4AICMFZ.js.map +0 -1
- package/dist/chunk-N3H4DXY5.js +0 -342
- package/dist/chunk-N3H4DXY5.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/presence/registry.ts","../src/sheets-adapter.ts"],"names":["useState","useRef","useCallback","useMemo","useEffect"],"mappings":"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","file":"sheets-adapter.cjs","sourcesContent":["import type { ActivityFilter, AgentActivityEvent, AgentActivityListener } from \"./types\";\n\n/**\n * In-process registry of agent activity events. Bridges call `emitActivity`\n * after a tool runs; React hooks + the SSE relay subscribe via\n * `onActivity()`.\n *\n * Holds a short scrollback of recent events (default 200) so newly-mounted\n * subscribers can render the recent past — useful for activity-log UIs\n * that rejoin a session mid-stream.\n */\n\nconst HISTORY_CAP = 200;\n\nconst listeners = new Set<AgentActivityListener>();\nconst history: AgentActivityEvent[] = [];\n\n/** Emit an activity event. All current listeners receive it synchronously. */\nexport function emitActivity(event: AgentActivityEvent): void {\n history.push(event);\n if (history.length > HISTORY_CAP) history.splice(0, history.length - HISTORY_CAP);\n for (const l of listeners) l(event);\n}\n\n/**\n * Subscribe to all events (or a filtered subset). Returns an unsubscribe\n * function. Filter checks all provided keys with strict equality; omit a\n * key to ignore it.\n */\nexport function onActivity(listener: AgentActivityListener, filter?: ActivityFilter): () => void {\n const wrapped: AgentActivityListener = filter\n ? (e) => { if (matches(e, filter)) listener(e); }\n : listener;\n listeners.add(wrapped);\n return () => listeners.delete(wrapped);\n}\n\n/** Read the recent history (newest last). Optional filter. */\nexport function readActivityHistory(filter?: ActivityFilter): AgentActivityEvent[] {\n if (!filter) return history.slice();\n return history.filter((e) => matches(e, filter));\n}\n\n/** Wipe history + clear listeners. Test/teardown helper. */\nexport function resetActivityRegistry(): void {\n listeners.clear();\n history.length = 0;\n}\n\nfunction matches(e: AgentActivityEvent, f: ActivityFilter): boolean {\n if (f.agentId !== undefined && e.agentId !== f.agentId) return false;\n if (f.screenId !== undefined && e.target.screenId !== f.screenId) return false;\n if (f.kind !== undefined && e.target.kind !== f.kind) return false;\n return true;\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { SheetsBridgeAdapter } from \"./bridges/sheets\";\nimport { onActivity } from \"./presence/registry\";\nimport type { AgentActivityEvent } from \"./presence/types\";\n\n/**\n * Shared-session helpers for `@particle-academy/fancy-sheets`.\n *\n * fancy-sheets' `SheetWorkbook` is already controlled (`data` + `onChange`).\n * The two missing pieces for a clean shared-session experience are:\n *\n * 1. an adapter object the host can hand to {@link registerSheetsBridge}\n * without writing boilerplate, and\n * 2. a derived `CellHighlightMap` so agent edits visibly pulse on the\n * humans' screens — wired from the presence registry's per-bridge\n * activity stream.\n *\n * These are kept as host-side hooks (not part of the bridge itself) so\n * agent-integrations keeps zero hard deps on fancy-sheets. The host\n * imports SheetWorkbook directly and feeds these hooks' outputs into\n * its props.\n *\n * const wb = useSheetsAdapter(initial, { screenId: \"deal-sheet\" });\n * const highlights = useSheetsActivityHighlights({ screenId: \"deal-sheet\" });\n *\n * useEffect(() => {\n * const bridge = registerSheetsBridge(host, { adapter: wb.adapter });\n * return bridge.dispose;\n * }, [host, wb.adapter]);\n *\n * <SheetWorkbook\n * data={wb.workbook}\n * onChange={wb.setWorkbook}\n * highlights={highlights}\n * onActiveCellChange={wb.onActiveCellChange}\n * />\n */\n\n// Loose type mirror of fancy-sheets' WorkbookData — kept local so this\n// helper doesn't pull a runtime dep on the package. Apps using the helper\n// import the real `WorkbookData` from fancy-sheets and pass it through.\nexport type WorkbookLike = {\n sheets: Array<{ id: string; name: string; [k: string]: unknown }>;\n activeSheetId: string;\n};\n\nexport type SheetsAdapterOptions = {\n /** Tags the bridge's screen id so presence events route correctly. */\n screenId?: string;\n};\n\nexport type UseSheetsAdapterResult<W extends WorkbookLike> = {\n /** Controlled workbook state. Wire to `<SheetWorkbook data={…} />`. */\n workbook: W;\n /** Setter for the controlled state. Wire to `<SheetWorkbook onChange={…} />`. */\n setWorkbook: (next: W) => void;\n /** Wire to `<SheetWorkbook onActiveCellChange={…} />` to track focus. */\n onActiveCellChange: (address: string) => void;\n /** Stable adapter to hand to `registerSheetsBridge({ adapter })`. */\n adapter: SheetsBridgeAdapter;\n /** Imperative: set the active sheet + cell. Mirrors the adapter's hook. */\n setActiveCell: (sheetId: string, address: string) => void;\n /** Read-only: the address last focused (any source). */\n activeCell: string | null;\n};\n\n/**\n * useSheetsAdapter — one-liner glue between fancy-sheets' SheetWorkbook\n * and the sheets bridge.\n *\n * const wb = useSheetsAdapter(initialWorkbook, { screenId: \"...\" });\n *\n * useEffect(() => registerSheetsBridge(host, { adapter: wb.adapter }).dispose,\n * [host, wb.adapter]);\n *\n * <SheetWorkbook\n * data={wb.workbook}\n * onChange={wb.setWorkbook}\n * onActiveCellChange={wb.onActiveCellChange}\n * />\n */\nexport function useSheetsAdapter<W extends WorkbookLike>(\n initial: W,\n options: SheetsAdapterOptions = {},\n): UseSheetsAdapterResult<W> {\n const [workbook, setWorkbook] = useState<W>(initial);\n const [activeCell, setActiveCellState] = useState<string | null>(null);\n const workbookRef = useRef(workbook);\n workbookRef.current = workbook;\n\n const setActiveCell = useCallback((sheetId: string, address: string) => {\n setWorkbook((cur) => (cur.activeSheetId === sheetId ? cur : { ...cur, activeSheetId: sheetId }));\n setActiveCellState(address);\n }, []);\n\n const onActiveCellChange = useCallback((address: string) => {\n setActiveCellState(address);\n }, []);\n\n // Adapter must be stable across renders so the bridge's tool catalog\n // doesn't churn — bind it to refs that hold the latest state + setter.\n const setWorkbookRef = useRef(setWorkbook);\n setWorkbookRef.current = setWorkbook;\n\n const adapter = useMemo<SheetsBridgeAdapter>(\n () => ({\n screenId: options.screenId,\n getWorkbook: () => workbookRef.current as unknown as ReturnType<SheetsBridgeAdapter[\"getWorkbook\"]>,\n setWorkbook: (next) => setWorkbookRef.current(next as unknown as W),\n setActiveCell,\n }),\n [options.screenId, setActiveCell],\n );\n\n return {\n workbook,\n setWorkbook,\n onActiveCellChange,\n adapter,\n setActiveCell,\n activeCell,\n };\n}\n\n/**\n * Loose mirror of fancy-sheets' `CellHighlightMap`. Each key is a cell\n * address (`\"B12\"`); each value is the visual treatment to apply.\n */\nexport type SheetsCellHighlight = {\n color?: string;\n /** Background tint; if omitted, derived from `color` at low alpha. */\n background?: string;\n /** Optional label rendered in a chip on the cell. */\n label?: string;\n /** Optional className appended to the cell. */\n className?: string;\n};\n\nexport type SheetsCellHighlightMap = Record<string, SheetsCellHighlight>;\n\nexport type SheetsHighlightOptions = {\n /** Only include events for this screen (recommended). */\n screenId?: string;\n /** Highlight TTL in ms before a hit fades from the map. Default 2200. */\n ttlMs?: number;\n};\n\n/**\n * useSheetsActivityHighlights — subscribe to the presence registry,\n * produce a CellHighlightMap reflecting recent sheet-bridge activity.\n *\n * Pass the result straight into `<SheetWorkbook highlights={…} />`. Each\n * agent edit pulses in the agent's color for `ttlMs` then fades out.\n *\n * The bridge's target shape is `${sheetId}!${address}` — this hook\n * filters for the currently-active sheet and exposes only its cells.\n *\n * const highlights = useSheetsActivityHighlights({ screenId: \"deal-sheet\" });\n * <SheetWorkbook highlights={highlights} … />\n */\nexport function useSheetsActivityHighlights(\n options: SheetsHighlightOptions = {},\n): SheetsCellHighlightMap {\n const ttlMs = options.ttlMs ?? 2200;\n const screenId = options.screenId;\n const [, force] = useState(0);\n const hitsRef = useRef<\n Map<string, { event: AgentActivityEvent; expiresAt: number }>\n >(new Map());\n\n useEffect(() => {\n const off = onActivity((event) => {\n if (event.target?.kind !== \"sheet\") return;\n if (screenId && event.target.screenId && event.target.screenId !== screenId) return;\n const elementId = event.target.elementId;\n if (!elementId || !elementId.includes(\"!\")) return;\n hitsRef.current.set(elementId, { event, expiresAt: Date.now() + ttlMs });\n force((n) => n + 1);\n });\n return off;\n }, [screenId, ttlMs]);\n\n // Periodic GC — drop expired entries and force a re-render.\n useEffect(() => {\n const t = window.setInterval(() => {\n const now = Date.now();\n let dirty = false;\n for (const [k, v] of hitsRef.current) {\n if (v.expiresAt < now) {\n hitsRef.current.delete(k);\n dirty = true;\n }\n }\n if (dirty) force((n) => n + 1);\n }, 500);\n return () => window.clearInterval(t);\n }, []);\n\n // Re-derived on every render — the listener + GC timer above call\n // `force` so renders happen exactly when the map changes.\n const out: SheetsCellHighlightMap = {};\n for (const [elementId, { event }] of hitsRef.current) {\n const idx = elementId.indexOf(\"!\");\n const address = elementId.slice(idx + 1);\n if (!address) continue;\n const color = event.agentColor ?? \"#a855f7\";\n out[address] = {\n color,\n background: color + \"33\",\n label: event.agentName ?? event.agentId ?? \"agent\",\n };\n }\n return out;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/presence/registry.ts","../src/sheets-adapter.ts"],"names":["useState","useRef","useCallback","useMemo","useEffect"],"mappings":"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","file":"sheets-adapter.cjs","sourcesContent":["import type { ActivityFilter, AgentActivityEvent, AgentActivityListener } from \"./types\";\n\n/**\n * In-process registry of agent activity events. Bridges call `emitActivity`\n * after a tool runs; React hooks + the SSE relay subscribe via\n * `onActivity()`.\n *\n * Holds a short scrollback of recent events (default 200) so newly-mounted\n * subscribers can render the recent past — useful for activity-log UIs\n * that rejoin a session mid-stream.\n */\n\nconst HISTORY_CAP = 200;\n\nconst listeners = new Set<AgentActivityListener>();\nconst history: AgentActivityEvent[] = [];\n\n/** Emit an activity event. All current listeners receive it synchronously. */\nexport function emitActivity(event: AgentActivityEvent): void {\n history.push(event);\n if (history.length > HISTORY_CAP) history.splice(0, history.length - HISTORY_CAP);\n for (const l of listeners) l(event);\n}\n\n/**\n * Subscribe to all events (or a filtered subset). Returns an unsubscribe\n * function. Filter checks all provided keys with strict equality; omit a\n * key to ignore it.\n */\nexport function onActivity(listener: AgentActivityListener, filter?: ActivityFilter): () => void {\n const wrapped: AgentActivityListener = filter\n ? (e) => { if (matches(e, filter)) listener(e); }\n : listener;\n listeners.add(wrapped);\n return () => listeners.delete(wrapped);\n}\n\n/** Read the recent history (newest last). Optional filter. */\nexport function readActivityHistory(filter?: ActivityFilter): AgentActivityEvent[] {\n if (!filter) return history.slice();\n return history.filter((e) => matches(e, filter));\n}\n\n/** Wipe history + clear listeners. Test/teardown helper. */\nexport function resetActivityRegistry(): void {\n listeners.clear();\n history.length = 0;\n}\n\nfunction matches(e: AgentActivityEvent, f: ActivityFilter): boolean {\n if (f.agentId !== undefined && e.agentId !== f.agentId) return false;\n if (f.screenId !== undefined && e.target.screenId !== f.screenId) return false;\n if (f.kind !== undefined && e.target.kind !== f.kind) return false;\n return true;\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { SheetsBridgeAdapter } from \"./bridges/sheets\";\nimport { onActivity } from \"./presence/registry\";\nimport type { AgentActivityEvent } from \"./presence/types\";\n\n/**\n * Shared-session helpers for `@particle-academy/fancy-sheets`.\n *\n * fancy-sheets' `SheetWorkbook` is already controlled (`data` + `onChange`).\n * The two missing pieces for a clean shared-session experience are:\n *\n * 1. an adapter object the host can hand to {@link registerSheetsBridge}\n * without writing boilerplate, and\n * 2. a derived `CellHighlightMap` so agent edits visibly pulse on the\n * humans' screens — wired from the presence registry's per-bridge\n * activity stream.\n *\n * These are kept as host-side hooks (not part of the bridge itself) so\n * agent-integrations keeps zero hard deps on fancy-sheets. The host\n * imports SheetWorkbook directly and feeds these hooks' outputs into\n * its props.\n *\n * const wb = useSheetsAdapter(initial, { screenId: \"deal-sheet\" });\n * const highlights = useSheetsActivityHighlights({ screenId: \"deal-sheet\" });\n *\n * useEffect(() => {\n * const bridge = registerSheetsBridge(host, { adapter: wb.adapter });\n * return bridge.dispose;\n * }, [host, wb.adapter]);\n *\n * <SheetWorkbook\n * data={wb.workbook}\n * onChange={wb.setWorkbook}\n * highlights={highlights}\n * onActiveCellChange={wb.onActiveCellChange}\n * />\n */\n\n// Loose structural mirror of fancy-sheets' WorkbookData — kept local so\n// this helper doesn't pull a runtime dep on the package. The constraint\n// only names the fields the hook itself reads (`sheets` + `activeSheetId`);\n// it does NOT add a `[k: string]: unknown` index signature, so consumers\n// can pass the real `WorkbookData` from fancy-sheets through without\n// triggering an \"index signature missing\" error.\nexport type WorkbookLike = {\n sheets: Array<{ id: string; name: string }>;\n activeSheetId: string;\n};\n\nexport type SheetsAdapterOptions = {\n /** Tags the bridge's screen id so presence events route correctly. */\n screenId?: string;\n};\n\nexport type UseSheetsAdapterResult<W extends WorkbookLike> = {\n /** Controlled workbook state. Wire to `<SheetWorkbook data={…} />`. */\n workbook: W;\n /** Setter for the controlled state. Wire to `<SheetWorkbook onChange={…} />`. */\n setWorkbook: (next: W) => void;\n /** Wire to `<SheetWorkbook onActiveCellChange={…} />` to track focus. */\n onActiveCellChange: (address: string) => void;\n /** Stable adapter to hand to `registerSheetsBridge({ adapter })`. */\n adapter: SheetsBridgeAdapter;\n /** Imperative: set the active sheet + cell. Mirrors the adapter's hook. */\n setActiveCell: (sheetId: string, address: string) => void;\n /** Read-only: the address last focused (any source). */\n activeCell: string | null;\n};\n\n/**\n * useSheetsAdapter — one-liner glue between fancy-sheets' SheetWorkbook\n * and the sheets bridge.\n *\n * const wb = useSheetsAdapter(initialWorkbook, { screenId: \"...\" });\n *\n * useEffect(() => registerSheetsBridge(host, { adapter: wb.adapter }).dispose,\n * [host, wb.adapter]);\n *\n * <SheetWorkbook\n * data={wb.workbook}\n * onChange={wb.setWorkbook}\n * onActiveCellChange={wb.onActiveCellChange}\n * />\n */\nexport function useSheetsAdapter<W extends WorkbookLike>(\n initial: W,\n options: SheetsAdapterOptions = {},\n): UseSheetsAdapterResult<W> {\n const [workbook, setWorkbook] = useState<W>(initial);\n const [activeCell, setActiveCellState] = useState<string | null>(null);\n const workbookRef = useRef(workbook);\n workbookRef.current = workbook;\n\n const setActiveCell = useCallback((sheetId: string, address: string) => {\n setWorkbook((cur) => (cur.activeSheetId === sheetId ? cur : { ...cur, activeSheetId: sheetId }));\n setActiveCellState(address);\n }, []);\n\n const onActiveCellChange = useCallback((address: string) => {\n setActiveCellState(address);\n }, []);\n\n // Adapter must be stable across renders so the bridge's tool catalog\n // doesn't churn — bind it to refs that hold the latest state + setter.\n const setWorkbookRef = useRef(setWorkbook);\n setWorkbookRef.current = setWorkbook;\n\n const adapter = useMemo<SheetsBridgeAdapter>(\n () => ({\n screenId: options.screenId,\n getWorkbook: () => workbookRef.current as unknown as ReturnType<SheetsBridgeAdapter[\"getWorkbook\"]>,\n setWorkbook: (next) => setWorkbookRef.current(next as unknown as W),\n setActiveCell,\n }),\n [options.screenId, setActiveCell],\n );\n\n return {\n workbook,\n setWorkbook,\n onActiveCellChange,\n adapter,\n setActiveCell,\n activeCell,\n };\n}\n\n/**\n * Mirror of fancy-sheets' `CellHighlight` — structurally identical so the\n * map returned from {@link useSheetsActivityHighlights} can be passed\n * straight into `<SheetWorkbook highlights={…} />` without any casts.\n *\n * Fields match `@particle-academy/fancy-sheets` exactly:\n * - `color: string` (required) — border/outline color\n * - `backgroundColor?: string` — auto-derived from `color` if omitted\n * - `label?: string` — small badge in the cell's top-left corner\n */\nexport type SheetsCellHighlight = {\n /** Border/outline color (any CSS color value). Required. */\n color: string;\n /** Background tint; if omitted, derived from `color` at low alpha. */\n backgroundColor?: string;\n /** Optional label rendered in a chip on the cell. */\n label?: string;\n};\n\nexport type SheetsCellHighlightMap = Record<string, SheetsCellHighlight>;\n\nexport type SheetsHighlightOptions = {\n /** Only include events for this screen (recommended). */\n screenId?: string;\n /** Highlight TTL in ms before a hit fades from the map. Default 2200. */\n ttlMs?: number;\n};\n\n/**\n * useSheetsActivityHighlights — subscribe to the presence registry,\n * produce a CellHighlightMap reflecting recent sheet-bridge activity.\n *\n * Pass the result straight into `<SheetWorkbook highlights={…} />`. Each\n * agent edit pulses in the agent's color for `ttlMs` then fades out.\n *\n * The bridge's target shape is `${sheetId}!${address}` — this hook\n * filters for the currently-active sheet and exposes only its cells.\n *\n * const highlights = useSheetsActivityHighlights({ screenId: \"deal-sheet\" });\n * <SheetWorkbook highlights={highlights} … />\n */\nexport function useSheetsActivityHighlights(\n options: SheetsHighlightOptions = {},\n): SheetsCellHighlightMap {\n const ttlMs = options.ttlMs ?? 2200;\n const screenId = options.screenId;\n const [, force] = useState(0);\n const hitsRef = useRef<\n Map<string, { event: AgentActivityEvent; expiresAt: number }>\n >(new Map());\n\n useEffect(() => {\n const off = onActivity((event) => {\n if (event.target?.kind !== \"sheet\") return;\n if (screenId && event.target.screenId && event.target.screenId !== screenId) return;\n const elementId = event.target.elementId;\n if (!elementId || !elementId.includes(\"!\")) return;\n hitsRef.current.set(elementId, { event, expiresAt: Date.now() + ttlMs });\n force((n) => n + 1);\n });\n return off;\n }, [screenId, ttlMs]);\n\n // Periodic GC — drop expired entries and force a re-render.\n useEffect(() => {\n const t = window.setInterval(() => {\n const now = Date.now();\n let dirty = false;\n for (const [k, v] of hitsRef.current) {\n if (v.expiresAt < now) {\n hitsRef.current.delete(k);\n dirty = true;\n }\n }\n if (dirty) force((n) => n + 1);\n }, 500);\n return () => window.clearInterval(t);\n }, []);\n\n // Re-derived on every render — the listener + GC timer above call\n // `force` so renders happen exactly when the map changes.\n const out: SheetsCellHighlightMap = {};\n for (const [elementId, { event }] of hitsRef.current) {\n const idx = elementId.indexOf(\"!\");\n const address = elementId.slice(idx + 1);\n if (!address) continue;\n const color = event.agentColor ?? \"#a855f7\";\n out[address] = {\n color,\n backgroundColor: color + \"33\",\n label: event.agentName ?? event.agentId ?? \"agent\",\n };\n }\n return out;\n}\n"]}
|
|
@@ -39,7 +39,6 @@ type WorkbookLike = {
|
|
|
39
39
|
sheets: Array<{
|
|
40
40
|
id: string;
|
|
41
41
|
name: string;
|
|
42
|
-
[k: string]: unknown;
|
|
43
42
|
}>;
|
|
44
43
|
activeSheetId: string;
|
|
45
44
|
};
|
|
@@ -78,17 +77,22 @@ type UseSheetsAdapterResult<W extends WorkbookLike> = {
|
|
|
78
77
|
*/
|
|
79
78
|
declare function useSheetsAdapter<W extends WorkbookLike>(initial: W, options?: SheetsAdapterOptions): UseSheetsAdapterResult<W>;
|
|
80
79
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
80
|
+
* Mirror of fancy-sheets' `CellHighlight` — structurally identical so the
|
|
81
|
+
* map returned from {@link useSheetsActivityHighlights} can be passed
|
|
82
|
+
* straight into `<SheetWorkbook highlights={…} />` without any casts.
|
|
83
|
+
*
|
|
84
|
+
* Fields match `@particle-academy/fancy-sheets` exactly:
|
|
85
|
+
* - `color: string` (required) — border/outline color
|
|
86
|
+
* - `backgroundColor?: string` — auto-derived from `color` if omitted
|
|
87
|
+
* - `label?: string` — small badge in the cell's top-left corner
|
|
83
88
|
*/
|
|
84
89
|
type SheetsCellHighlight = {
|
|
85
|
-
color
|
|
90
|
+
/** Border/outline color (any CSS color value). Required. */
|
|
91
|
+
color: string;
|
|
86
92
|
/** Background tint; if omitted, derived from `color` at low alpha. */
|
|
87
|
-
|
|
93
|
+
backgroundColor?: string;
|
|
88
94
|
/** Optional label rendered in a chip on the cell. */
|
|
89
95
|
label?: string;
|
|
90
|
-
/** Optional className appended to the cell. */
|
|
91
|
-
className?: string;
|
|
92
96
|
};
|
|
93
97
|
type SheetsCellHighlightMap = Record<string, SheetsCellHighlight>;
|
|
94
98
|
type SheetsHighlightOptions = {
|
package/dist/sheets-adapter.d.ts
CHANGED
|
@@ -39,7 +39,6 @@ type WorkbookLike = {
|
|
|
39
39
|
sheets: Array<{
|
|
40
40
|
id: string;
|
|
41
41
|
name: string;
|
|
42
|
-
[k: string]: unknown;
|
|
43
42
|
}>;
|
|
44
43
|
activeSheetId: string;
|
|
45
44
|
};
|
|
@@ -78,17 +77,22 @@ type UseSheetsAdapterResult<W extends WorkbookLike> = {
|
|
|
78
77
|
*/
|
|
79
78
|
declare function useSheetsAdapter<W extends WorkbookLike>(initial: W, options?: SheetsAdapterOptions): UseSheetsAdapterResult<W>;
|
|
80
79
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
80
|
+
* Mirror of fancy-sheets' `CellHighlight` — structurally identical so the
|
|
81
|
+
* map returned from {@link useSheetsActivityHighlights} can be passed
|
|
82
|
+
* straight into `<SheetWorkbook highlights={…} />` without any casts.
|
|
83
|
+
*
|
|
84
|
+
* Fields match `@particle-academy/fancy-sheets` exactly:
|
|
85
|
+
* - `color: string` (required) — border/outline color
|
|
86
|
+
* - `backgroundColor?: string` — auto-derived from `color` if omitted
|
|
87
|
+
* - `label?: string` — small badge in the cell's top-left corner
|
|
83
88
|
*/
|
|
84
89
|
type SheetsCellHighlight = {
|
|
85
|
-
color
|
|
90
|
+
/** Border/outline color (any CSS color value). Required. */
|
|
91
|
+
color: string;
|
|
86
92
|
/** Background tint; if omitted, derived from `color` at low alpha. */
|
|
87
|
-
|
|
93
|
+
backgroundColor?: string;
|
|
88
94
|
/** Optional label rendered in a chip on the cell. */
|
|
89
95
|
label?: string;
|
|
90
|
-
/** Optional className appended to the cell. */
|
|
91
|
-
className?: string;
|
|
92
96
|
};
|
|
93
97
|
type SheetsCellHighlightMap = Record<string, SheetsCellHighlight>;
|
|
94
98
|
type SheetsHighlightOptions = {
|
package/dist/sheets-adapter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { useSheetsActivityHighlights, useSheetsAdapter } from './chunk-
|
|
1
|
+
export { useSheetsActivityHighlights, useSheetsAdapter } from './chunk-5XELJIJR.js';
|
|
2
2
|
import './chunk-JU2N4KK6.js';
|
|
3
3
|
//# sourceMappingURL=sheets-adapter.js.map
|
|
4
4
|
//# sourceMappingURL=sheets-adapter.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-token utilities. The token is a high-entropy secret; possession
|
|
3
|
+
* grants read/write on the session. We don't HMAC frames — frames carry
|
|
4
|
+
* the token directly (which is fine for in-process / same-origin / TLS
|
|
5
|
+
* transports). For lower-trust transports, host apps can layer signing
|
|
6
|
+
* on top of the BroadcastChannelTransport.
|
|
7
|
+
*/
|
|
8
|
+
type SessionDescriptor = {
|
|
9
|
+
/** Stable session identifier. Channel name = `fai:share:${id}`. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Secret token. Treat as a password — anyone with it can read/write. */
|
|
12
|
+
token: string;
|
|
13
|
+
/** Pretty hash for display (first 8 chars of token). */
|
|
14
|
+
display: string;
|
|
15
|
+
};
|
|
16
|
+
declare function createSessionDescriptor(): SessionDescriptor;
|
|
17
|
+
declare function describeSession(id: string, token: string): SessionDescriptor;
|
|
18
|
+
/** Build the shareable URL for the current page (preserves path, adds session+token). */
|
|
19
|
+
declare function buildShareUrl(descriptor: SessionDescriptor, baseUrl?: string): string;
|
|
20
|
+
/** Build the JSON config form (suitable for Claude Desktop / Cline / etc.). */
|
|
21
|
+
declare function buildShareConfig(descriptor: SessionDescriptor, transport?: string): {
|
|
22
|
+
name: string;
|
|
23
|
+
transport: string;
|
|
24
|
+
session: string;
|
|
25
|
+
token: string;
|
|
26
|
+
channel: string;
|
|
27
|
+
protocol_version: string;
|
|
28
|
+
};
|
|
29
|
+
/** Read session descriptor from current URL, or null if not a shared link. */
|
|
30
|
+
declare function readSessionFromUrl(): SessionDescriptor | null;
|
|
31
|
+
/** Constant-time string compare so a mismatched token leaks no timing info. */
|
|
32
|
+
declare function constantTimeEqual(a: string, b: string): boolean;
|
|
33
|
+
|
|
34
|
+
export { type SessionDescriptor as S, buildShareUrl as a, buildShareConfig as b, createSessionDescriptor as c, describeSession as d, constantTimeEqual as e, readSessionFromUrl as r };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-token utilities. The token is a high-entropy secret; possession
|
|
3
|
+
* grants read/write on the session. We don't HMAC frames — frames carry
|
|
4
|
+
* the token directly (which is fine for in-process / same-origin / TLS
|
|
5
|
+
* transports). For lower-trust transports, host apps can layer signing
|
|
6
|
+
* on top of the BroadcastChannelTransport.
|
|
7
|
+
*/
|
|
8
|
+
type SessionDescriptor = {
|
|
9
|
+
/** Stable session identifier. Channel name = `fai:share:${id}`. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Secret token. Treat as a password — anyone with it can read/write. */
|
|
12
|
+
token: string;
|
|
13
|
+
/** Pretty hash for display (first 8 chars of token). */
|
|
14
|
+
display: string;
|
|
15
|
+
};
|
|
16
|
+
declare function createSessionDescriptor(): SessionDescriptor;
|
|
17
|
+
declare function describeSession(id: string, token: string): SessionDescriptor;
|
|
18
|
+
/** Build the shareable URL for the current page (preserves path, adds session+token). */
|
|
19
|
+
declare function buildShareUrl(descriptor: SessionDescriptor, baseUrl?: string): string;
|
|
20
|
+
/** Build the JSON config form (suitable for Claude Desktop / Cline / etc.). */
|
|
21
|
+
declare function buildShareConfig(descriptor: SessionDescriptor, transport?: string): {
|
|
22
|
+
name: string;
|
|
23
|
+
transport: string;
|
|
24
|
+
session: string;
|
|
25
|
+
token: string;
|
|
26
|
+
channel: string;
|
|
27
|
+
protocol_version: string;
|
|
28
|
+
};
|
|
29
|
+
/** Read session descriptor from current URL, or null if not a shared link. */
|
|
30
|
+
declare function readSessionFromUrl(): SessionDescriptor | null;
|
|
31
|
+
/** Constant-time string compare so a mismatched token leaks no timing info. */
|
|
32
|
+
declare function constantTimeEqual(a: string, b: string): boolean;
|
|
33
|
+
|
|
34
|
+
export { type SessionDescriptor as S, buildShareUrl as a, buildShareConfig as b, createSessionDescriptor as c, describeSession as d, constantTimeEqual as e, readSessionFromUrl as r };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Relay server
|
|
2
|
+
|
|
3
|
+
Ships with `@particle-academy/agent-integrations` as of `0.6.0`. The relay is the
|
|
4
|
+
server-side half of the SSE+POST tunnel documented in [relay-protocol.md](./relay-protocol.md) —
|
|
5
|
+
it shuttles JSON-RPC frames between a browser-hosted `MicroMcpServer` and any
|
|
6
|
+
external MCP client (Claude Code, Cursor, Claude Desktop, custom agents).
|
|
7
|
+
|
|
8
|
+
The browser is the *server* in this model — it owns the tools and the state.
|
|
9
|
+
The relay is purely a broker. No tools run server-side; no state persists
|
|
10
|
+
across restarts.
|
|
11
|
+
|
|
12
|
+
## When you need a relay
|
|
13
|
+
|
|
14
|
+
- **In-process agents** (an AI assistant rendered inside the same React tree)
|
|
15
|
+
don't need a relay — use `attachInProcess(server)` directly. The relay is for
|
|
16
|
+
*external* agents whose process can't reach the browser tab.
|
|
17
|
+
- **End-user-facing demos** where a visitor pastes a session URL into Claude
|
|
18
|
+
Code → the relay is hosted somewhere reachable from both the browser and the
|
|
19
|
+
agent's machine.
|
|
20
|
+
|
|
21
|
+
## Three ways to run it
|
|
22
|
+
|
|
23
|
+
### 1. `npx` — local dev / one-off prod
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx -p @particle-academy/agent-integrations agent-integrations-relay --port 8787
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
End-to-end smoke test:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
curl http://localhost:8787/ # health
|
|
33
|
+
curl -X POST -H 'content-type: application/json' \
|
|
34
|
+
-d '{"session":"demo-001","token":"abcdef0123456789abcdef0123456789"}' \
|
|
35
|
+
http://localhost:8787/register
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
CLI flags (or matching env vars `PORT`, `HOST`, `PREFIX`, `TTL_MS`, `CORS_ALLOW_ORIGIN`):
|
|
39
|
+
|
|
40
|
+
| Flag | Default | What |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `--port <n>` | `8787` | Listen port. |
|
|
43
|
+
| `--host <addr>` | `0.0.0.0` | Bind address. |
|
|
44
|
+
| `--prefix <path>` | `""` | URL path prefix (e.g. `/mcp-relay`) when behind a reverse proxy. |
|
|
45
|
+
| `--ttl-ms <n>` | `14_400_000` (4h) | Session inactivity timeout. |
|
|
46
|
+
| `--cors <origin>` | `*` | `Access-Control-Allow-Origin` header value. |
|
|
47
|
+
|
|
48
|
+
### 2. Embed in an existing Node HTTP framework
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { createNodeRelay } from "@particle-academy/agent-integrations/relay-server";
|
|
52
|
+
|
|
53
|
+
const relay = createNodeRelay({ pathPrefix: "/mcp-relay", corsAllowOrigin: "*" });
|
|
54
|
+
|
|
55
|
+
app.post("/mcp-relay/register", (req, res) => relay.register(req, res));
|
|
56
|
+
app.post("/mcp-relay/:s/inbox", (req, res) => relay.inbox(req, res));
|
|
57
|
+
app.post("/mcp-relay/:s/outbox", (req, res) => relay.outbox(req, res));
|
|
58
|
+
app.get ("/mcp-relay/:s/events", (req, res) => relay.events(req, res));
|
|
59
|
+
app.post("/mcp-relay/:s/unregister", (req, res) => relay.unregister(req, res));
|
|
60
|
+
|
|
61
|
+
// Or a single fall-through handler for routers that don't need per-route control:
|
|
62
|
+
app.use("/mcp-relay", (req, res) => relay.handler(req, res));
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 3. Docker
|
|
66
|
+
|
|
67
|
+
A `Dockerfile` ships in the package. Build + run:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git clone https://github.com/Particle-Academy/agent-integrations
|
|
71
|
+
cd agent-integrations
|
|
72
|
+
npm install
|
|
73
|
+
npm run build
|
|
74
|
+
docker build -t agent-integrations-relay .
|
|
75
|
+
docker run -p 8787:8787 agent-integrations-relay
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Deploy targets that just want a container:
|
|
79
|
+
|
|
80
|
+
- **Fly.io:** `fly launch --image agent-integrations-relay --internal-port 8787`
|
|
81
|
+
- **Railway:** `railway up` after committing the Dockerfile
|
|
82
|
+
- **Render:** point a Web Service at the Dockerfile, expose 8787
|
|
83
|
+
- **Cloud Run:** `gcloud run deploy --image agent-integrations-relay --port 8787 --allow-unauthenticated`
|
|
84
|
+
|
|
85
|
+
## Wire protocol
|
|
86
|
+
|
|
87
|
+
Same shape every consumer expects:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
POST {prefix}/register body: { session, token } → { ok }
|
|
91
|
+
POST {prefix}/{session}/inbox?token=… body: JSON-RPC frame → { ok }
|
|
92
|
+
POST {prefix}/{session}/outbox?token=… body: JSON-RPC frame → { ok }
|
|
93
|
+
GET {prefix}/{session}/events?token=…&direction=inbound|outbound
|
|
94
|
+
SSE stream of `event: mcp\ndata: …\n\n`
|
|
95
|
+
POST {prefix}/{session}/unregister?token=… → { ok }
|
|
96
|
+
GET {prefix}/ healthcheck → 200
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The browser opens an `inbound` SSE subscription; external agents open `outbound`.
|
|
100
|
+
Both POST JSON-RPC frames at their own direction's inbox/outbox.
|
|
101
|
+
|
|
102
|
+
## Replacing the in-memory store
|
|
103
|
+
|
|
104
|
+
The default broker holds session state in a `Map`. To run multiple relay
|
|
105
|
+
processes behind a load balancer, swap the store:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { RelayBroker, type Store } from "@particle-academy/agent-integrations/relay-server";
|
|
109
|
+
|
|
110
|
+
class RedisStore implements Store { /* ... */ }
|
|
111
|
+
|
|
112
|
+
const broker = new RelayBroker({ store: new RedisStore(/* … */) });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Frame fan-out within a single process is still in-memory; for multi-instance
|
|
116
|
+
correctness wire frames through a pub/sub (Redis Streams, NATS, etc.) by
|
|
117
|
+
extending the broker or running an instance per session-id-prefix.
|
|
118
|
+
|
|
119
|
+
## Security notes
|
|
120
|
+
|
|
121
|
+
- **Token comparison is timing-safe** (`crypto.timingSafeEqual`).
|
|
122
|
+
- **Sessions auto-expire** after `ttlMs` inactivity; every authenticated touch
|
|
123
|
+
slides the TTL forward.
|
|
124
|
+
- **Payload caps** — individual frames are rejected past 256 KB.
|
|
125
|
+
- The relay carries opaque frames; auth is your session token. Tighter access
|
|
126
|
+
control (per-IP rate limit, allowlist) belongs in your reverse proxy layer.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@particle-academy/agent-integrations",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "MCP-driven agent presence in collab sessions: per-session micro-MCP server, pluggable bridges to fancy-* packages, and agent UX components (panel + on-canvas cursor).",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,18 +12,74 @@
|
|
|
12
12
|
"main": "./dist/index.cjs",
|
|
13
13
|
"module": "./dist/index.js",
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
|
+
"bin": {
|
|
16
|
+
"agent-integrations-relay": "./dist/relay-server-cli.js",
|
|
17
|
+
"ai-relay": "./dist/relay-server-cli.js"
|
|
18
|
+
},
|
|
15
19
|
"exports": {
|
|
16
20
|
".": {
|
|
17
21
|
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
18
22
|
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
|
|
19
23
|
},
|
|
20
24
|
"./mcp": {
|
|
21
|
-
"import": { "types": "./dist/mcp.d.ts", "default": "./dist/mcp.js" },
|
|
22
|
-
"require": { "types": "./dist/mcp.d.cts", "default": "./dist/mcp.cjs" }
|
|
25
|
+
"import": { "types": "./dist/mcp/index.d.ts", "default": "./dist/mcp.js" },
|
|
26
|
+
"require": { "types": "./dist/mcp/index.d.cts", "default": "./dist/mcp.cjs" }
|
|
23
27
|
},
|
|
24
28
|
"./bridges/whiteboard": {
|
|
25
|
-
"import": { "types": "./dist/bridges
|
|
26
|
-
"require": { "types": "./dist/bridges
|
|
29
|
+
"import": { "types": "./dist/bridges/whiteboard.d.ts", "default": "./dist/bridges-whiteboard.js" },
|
|
30
|
+
"require": { "types": "./dist/bridges/whiteboard.d.cts", "default": "./dist/bridges-whiteboard.cjs" }
|
|
31
|
+
},
|
|
32
|
+
"./bridges/flow": {
|
|
33
|
+
"import": { "types": "./dist/bridges/flow.d.ts", "default": "./dist/bridges-flow.js" },
|
|
34
|
+
"require": { "types": "./dist/bridges/flow.d.cts", "default": "./dist/bridges-flow.cjs" }
|
|
35
|
+
},
|
|
36
|
+
"./bridges/forms": {
|
|
37
|
+
"import": { "types": "./dist/bridges/forms.d.ts", "default": "./dist/bridges-forms.js" },
|
|
38
|
+
"require": { "types": "./dist/bridges/forms.d.cts", "default": "./dist/bridges-forms.cjs" }
|
|
39
|
+
},
|
|
40
|
+
"./bridges/sheets": {
|
|
41
|
+
"import": { "types": "./dist/bridges/sheets.d.ts", "default": "./dist/bridges-sheets.js" },
|
|
42
|
+
"require": { "types": "./dist/bridges/sheets.d.cts", "default": "./dist/bridges-sheets.cjs" }
|
|
43
|
+
},
|
|
44
|
+
"./bridges/code": {
|
|
45
|
+
"import": { "types": "./dist/bridges/code.d.ts", "default": "./dist/bridges-code.js" },
|
|
46
|
+
"require": { "types": "./dist/bridges/code.d.cts", "default": "./dist/bridges-code.cjs" }
|
|
47
|
+
},
|
|
48
|
+
"./bridges/charts": {
|
|
49
|
+
"import": { "types": "./dist/bridges/charts.d.ts", "default": "./dist/bridges-charts.js" },
|
|
50
|
+
"require": { "types": "./dist/bridges/charts.d.cts", "default": "./dist/bridges-charts.cjs" }
|
|
51
|
+
},
|
|
52
|
+
"./bridges/scene": {
|
|
53
|
+
"import": { "types": "./dist/bridges/scene.d.ts", "default": "./dist/bridges-scene.js" },
|
|
54
|
+
"require": { "types": "./dist/bridges/scene.d.cts", "default": "./dist/bridges-scene.cjs" }
|
|
55
|
+
},
|
|
56
|
+
"./bridges/screens": {
|
|
57
|
+
"import": { "types": "./dist/bridges/screens.d.ts", "default": "./dist/bridges-screens.js" },
|
|
58
|
+
"require": { "types": "./dist/bridges/screens.d.cts", "default": "./dist/bridges-screens.cjs" }
|
|
59
|
+
},
|
|
60
|
+
"./sheets-adapter": {
|
|
61
|
+
"import": { "types": "./dist/sheets-adapter.d.ts", "default": "./dist/sheets-adapter.js" },
|
|
62
|
+
"require": { "types": "./dist/sheets-adapter.d.cts", "default": "./dist/sheets-adapter.cjs" }
|
|
63
|
+
},
|
|
64
|
+
"./components/shared-whiteboard": {
|
|
65
|
+
"import": { "types": "./dist/components/SharedWhiteboard/index.d.ts", "default": "./dist/components-shared-whiteboard.js" },
|
|
66
|
+
"require": { "types": "./dist/components/SharedWhiteboard/index.d.cts", "default": "./dist/components-shared-whiteboard.cjs" }
|
|
67
|
+
},
|
|
68
|
+
"./presence": {
|
|
69
|
+
"import": { "types": "./dist/presence/index.d.ts", "default": "./dist/presence.js" },
|
|
70
|
+
"require": { "types": "./dist/presence/index.d.cts", "default": "./dist/presence.cjs" }
|
|
71
|
+
},
|
|
72
|
+
"./undo": {
|
|
73
|
+
"import": { "types": "./dist/undo/index.d.ts", "default": "./dist/undo.js" },
|
|
74
|
+
"require": { "types": "./dist/undo/index.d.cts", "default": "./dist/undo.cjs" }
|
|
75
|
+
},
|
|
76
|
+
"./sharing": {
|
|
77
|
+
"import": { "types": "./dist/sharing/index.d.ts", "default": "./dist/sharing.js" },
|
|
78
|
+
"require": { "types": "./dist/sharing/index.d.cts", "default": "./dist/sharing.cjs" }
|
|
79
|
+
},
|
|
80
|
+
"./relay-server": {
|
|
81
|
+
"import": { "types": "./dist/relay-server/index.d.ts", "default": "./dist/relay-server.js" },
|
|
82
|
+
"require": { "types": "./dist/relay-server/index.d.cts", "default": "./dist/relay-server.cjs" }
|
|
27
83
|
},
|
|
28
84
|
"./styles.css": "./dist/styles.css"
|
|
29
85
|
},
|
|
@@ -40,14 +96,17 @@
|
|
|
40
96
|
"react": "^18.0.0 || ^19.0.0",
|
|
41
97
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
42
98
|
"@particle-academy/fancy-whiteboard": "^0.1.0",
|
|
43
|
-
"@particle-academy/fancy-flow": "^0.2.0"
|
|
99
|
+
"@particle-academy/fancy-flow": "^0.2.0",
|
|
100
|
+
"@particle-academy/fancy-sheets": "^0.1.0"
|
|
44
101
|
},
|
|
45
102
|
"peerDependenciesMeta": {
|
|
46
103
|
"@particle-academy/fancy-whiteboard": { "optional": true },
|
|
47
|
-
"@particle-academy/fancy-flow": { "optional": true }
|
|
104
|
+
"@particle-academy/fancy-flow": { "optional": true },
|
|
105
|
+
"@particle-academy/fancy-sheets": { "optional": true }
|
|
48
106
|
},
|
|
49
107
|
"devDependencies": {
|
|
50
108
|
"@particle-academy/fancy-whiteboard": "^0.1.5",
|
|
109
|
+
"@types/node": "^22.0.0",
|
|
51
110
|
"@types/react": "^19.0.0",
|
|
52
111
|
"@types/react-dom": "^19.0.0",
|
|
53
112
|
"react": "^19.0.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mcp/transports/in-process.ts","../src/mcp/transports/relay.ts"],"names":[],"mappings":";AAeO,IAAM,qBAAN,MAA8C;AAAA,EAA9C,WAAA,GAAA;AAEL,IAAA,IAAA,CAAQ,SAAA,uBAAgB,GAAA,EAAmC;AAAA,EAAA;AAAA;AAAA,EAG3D,WAAW,MAAA,EAA8B;AACvC,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AAAA;AAAA,EAGA,KAAK,OAAA,EAA+B;AAClC,IAAA,KAAA,MAAW,CAAA,IAAK,IAAA,CAAK,SAAA,EAAW,CAAA,CAAE,OAAO,CAAA;AAAA,EAC3C;AAAA;AAAA,EAGA,MAAM,QAAQ,OAAA,EAAwC;AACpD,IAAA,IAAI,CAAC,IAAA,CAAK,MAAA,EAAQ,MAAM,IAAI,MAAM,wCAAwC,CAAA;AAC1E,IAAA,MAAM,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,IAAA,EAAM,OAAO,CAAA;AAAA,EACzC;AAAA;AAAA,EAGA,gBAAgB,QAAA,EAAqD;AACnE,IAAA,IAAA,CAAK,SAAA,CAAU,IAAI,QAAQ,CAAA;AAC3B,IAAA,OAAO,MAAM,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,QAAQ,CAAA;AAAA,EAC7C;AAAA,EAEA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,UAAU,KAAA,EAAM;AAAA,EACvB;AACF;AAKO,SAAS,gBAAgB,MAAA,EAA4C;AAC1E,EAAA,MAAM,SAAA,GAAY,IAAI,kBAAA,EAAmB;AACzC,EAAA,SAAA,CAAU,WAAW,MAAM,CAAA;AAC3B,EAAA,MAAA,CAAO,OAAO,SAAS,CAAA;AACvB,EAAA,OAAO,SAAA;AACT;;;AC7BO,IAAM,iBAAN,MAA0C;AAAA,EAE/C,YAAoB,OAAA,EAAuB;AAAvB,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAAA,EAAwB;AAAA,EAE5C,WAAW,MAAA,EAA8B;AACvC,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AAAA,EAEA,KAAK,OAAA,EAA+B;AAClC,IAAA,IAAA,CAAK,OAAA,CAAQ,aAAa,OAAO,CAAA;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,kBAAkB,OAAA,EAAiD;AACvE,IAAA,IAAI,CAAC,IAAA,CAAK,MAAA,EAAQ,MAAM,IAAI,MAAM,oCAAoC,CAAA;AACtE,IAAA,MAAM,UAAU,OAAO,OAAA,KAAY,WAC9B,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA,GACnB,OAAA;AACJ,IAAA,MAAM,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,IAAA,EAAM,OAAO,CAAA;AAAA,EACzC;AAAA,EAEA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,QAAQ,OAAA,IAAU;AAAA,EACzB;AACF;AAKO,SAAS,WAAA,CAAY,QAAwB,OAAA,EAAuC;AACzF,EAAA,MAAM,SAAA,GAAY,IAAI,cAAA,CAAe,OAAO,CAAA;AAC5C,EAAA,SAAA,CAAU,WAAW,MAAM,CAAA;AAC3B,EAAA,MAAA,CAAO,OAAO,SAAS,CAAA;AACvB,EAAA,OAAO,SAAA;AACT","file":"chunk-6LTKCNLF.js","sourcesContent":["import type { JsonRpcMessage } from \"../types\";\nimport type { MicroMcpServer, Transport } from \"../server\";\n\n/**\n * InProcessTransport — direct function-call wiring between an in-page MCP\n * client (e.g. an embedded chat agent) and a MicroMcpServer running in\n * the same JS context. No serialization, no network.\n *\n * Usage:\n *\n * const t = new InProcessTransport();\n * server.attach(t);\n * t.onServerMessage((msg) => { ... }); // client subscribes\n * t.send({ jsonrpc: \"2.0\", id: 1, method: \"tools/list\" }); // client → server\n */\nexport class InProcessTransport implements Transport {\n private server?: MicroMcpServer;\n private listeners = new Set<(msg: JsonRpcMessage) => void>();\n\n /** Bind to a server. Called from the client's setup, not directly. */\n bindServer(server: MicroMcpServer): void {\n this.server = server;\n }\n\n /** Server → client (delivered to subscribed listeners). */\n send(message: JsonRpcMessage): void {\n for (const l of this.listeners) l(message);\n }\n\n /** Client → server. Awaitable so callers can flush. */\n async deliver(message: JsonRpcMessage): Promise<void> {\n if (!this.server) throw new Error(\"InProcessTransport has no bound server\");\n await this.server.receive(this, message);\n }\n\n /** Subscribe to messages the server pushes to this client. */\n onServerMessage(listener: (msg: JsonRpcMessage) => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n close(): void {\n this.listeners.clear();\n }\n}\n\n/**\n * Convenience: create a server-attached in-process transport in one call.\n */\nexport function attachInProcess(server: MicroMcpServer): InProcessTransport {\n const transport = new InProcessTransport();\n transport.bindServer(server);\n server.attach(transport);\n return transport;\n}\n","import type { JsonRpcMessage } from \"../types\";\nimport type { MicroMcpServer, Transport } from \"../server\";\n\n/**\n * RelayTransport — wraps any duplex JSON-frame channel (e.g. a Reverb\n * websocket private channel, a WebRTC data channel) so external agents\n * can talk to a browser-side MicroMcpServer.\n *\n * The host app owns the actual channel. This class only handles framing\n * (JSON.stringify / JSON.parse) and the server contract.\n *\n * Channel contract:\n * - host calls `transport.deliverFromRemote(payload)` with each frame\n * it receives from the remote agent\n * - host implements `sendToRemote(frame)` so the transport can deliver\n * server → client frames outward\n *\n * See docs/relay-protocol.md for the wire format.\n */\nexport type RelayChannel = {\n sendToRemote: (frame: JsonRpcMessage) => void;\n /** Optional: notify the channel that the server is gone. */\n onClose?: () => void;\n};\n\nexport class RelayTransport implements Transport {\n private server?: MicroMcpServer;\n constructor(private channel: RelayChannel) {}\n\n bindServer(server: MicroMcpServer): void {\n this.server = server;\n }\n\n send(message: JsonRpcMessage): void {\n this.channel.sendToRemote(message);\n }\n\n /**\n * Host calls this with each frame received from the remote agent. Accepts\n * either a parsed object or a raw JSON string.\n */\n async deliverFromRemote(payload: JsonRpcMessage | string): Promise<void> {\n if (!this.server) throw new Error(\"RelayTransport has no bound server\");\n const message = typeof payload === \"string\"\n ? (JSON.parse(payload) as JsonRpcMessage)\n : payload;\n await this.server.receive(this, message);\n }\n\n close(): void {\n this.channel.onClose?.();\n }\n}\n\n/**\n * Convenience wiring. Returns the bound transport.\n */\nexport function attachRelay(server: MicroMcpServer, channel: RelayChannel): RelayTransport {\n const transport = new RelayTransport(channel);\n transport.bindServer(server);\n server.attach(transport);\n return transport;\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sheets-adapter.ts"],"names":[],"mappings":";;;AAiFO,SAAS,gBAAA,CACd,OAAA,EACA,OAAA,GAAgC,EAAC,EACN;AAC3B,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAI,SAAY,OAAO,CAAA;AACnD,EAAA,MAAM,CAAC,UAAA,EAAY,kBAAkB,CAAA,GAAI,SAAwB,IAAI,CAAA;AACrE,EAAA,MAAM,WAAA,GAAc,OAAO,QAAQ,CAAA;AACnC,EAAA,WAAA,CAAY,OAAA,GAAU,QAAA;AAEtB,EAAA,MAAM,aAAA,GAAgB,WAAA,CAAY,CAAC,OAAA,EAAiB,OAAA,KAAoB;AACtE,IAAA,WAAA,CAAY,CAAC,GAAA,KAAS,GAAA,CAAI,aAAA,KAAkB,OAAA,GAAU,GAAA,GAAM,EAAE,GAAG,GAAA,EAAK,aAAA,EAAe,OAAA,EAAU,CAAA;AAC/F,IAAA,kBAAA,CAAmB,OAAO,CAAA;AAAA,EAC5B,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,kBAAA,GAAqB,WAAA,CAAY,CAAC,OAAA,KAAoB;AAC1D,IAAA,kBAAA,CAAmB,OAAO,CAAA;AAAA,EAC5B,CAAA,EAAG,EAAE,CAAA;AAIL,EAAA,MAAM,cAAA,GAAiB,OAAO,WAAW,CAAA;AACzC,EAAA,cAAA,CAAe,OAAA,GAAU,WAAA;AAEzB,EAAA,MAAM,OAAA,GAAU,OAAA;AAAA,IACd,OAAO;AAAA,MACL,UAAU,OAAA,CAAQ,QAAA;AAAA,MAClB,WAAA,EAAa,MAAM,WAAA,CAAY,OAAA;AAAA,MAC/B,WAAA,EAAa,CAAC,IAAA,KAAS,cAAA,CAAe,QAAQ,IAAoB,CAAA;AAAA,MAClE;AAAA,KACF,CAAA;AAAA,IACA,CAAC,OAAA,CAAQ,QAAA,EAAU,aAAa;AAAA,GAClC;AAEA,EAAA,OAAO;AAAA,IACL,QAAA;AAAA,IACA,WAAA;AAAA,IACA,kBAAA;AAAA,IACA,OAAA;AAAA,IACA,aAAA;AAAA,IACA;AAAA,GACF;AACF;AAsCO,SAAS,2BAAA,CACd,OAAA,GAAkC,EAAC,EACX;AACxB,EAAA,MAAM,KAAA,GAAQ,QAAQ,KAAA,IAAS,IAAA;AAC/B,EAAA,MAAM,WAAW,OAAA,CAAQ,QAAA;AACzB,EAAA,MAAM,GAAG,KAAK,CAAA,GAAI,SAAS,CAAC,CAAA;AAC5B,EAAA,MAAM,OAAA,GAAU,MAAA,iBAEd,IAAI,GAAA,EAAK,CAAA;AAEX,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,GAAA,GAAM,UAAA,CAAW,CAAC,KAAA,KAAU;AAChC,MAAA,IAAI,KAAA,CAAM,MAAA,EAAQ,IAAA,KAAS,OAAA,EAAS;AACpC,MAAA,IAAI,YAAY,KAAA,CAAM,MAAA,CAAO,YAAY,KAAA,CAAM,MAAA,CAAO,aAAa,QAAA,EAAU;AAC7E,MAAA,MAAM,SAAA,GAAY,MAAM,MAAA,CAAO,SAAA;AAC/B,MAAA,IAAI,CAAC,SAAA,IAAa,CAAC,SAAA,CAAU,QAAA,CAAS,GAAG,CAAA,EAAG;AAC5C,MAAA,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,SAAA,EAAW,EAAE,KAAA,EAAO,WAAW,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA,EAAO,CAAA;AACvE,MAAA,KAAA,CAAM,CAAC,CAAA,KAAM,CAAA,GAAI,CAAC,CAAA;AAAA,IACpB,CAAC,CAAA;AACD,IAAA,OAAO,GAAA;AAAA,EACT,CAAA,EAAG,CAAC,QAAA,EAAU,KAAK,CAAC,CAAA;AAGpB,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,CAAA,GAAI,MAAA,CAAO,WAAA,CAAY,MAAM;AACjC,MAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,MAAA,IAAI,KAAA,GAAQ,KAAA;AACZ,MAAA,KAAA,MAAW,CAAC,CAAA,EAAG,CAAC,CAAA,IAAK,QAAQ,OAAA,EAAS;AACpC,QAAA,IAAI,CAAA,CAAE,YAAY,GAAA,EAAK;AACrB,UAAA,OAAA,CAAQ,OAAA,CAAQ,OAAO,CAAC,CAAA;AACxB,UAAA,KAAA,GAAQ,IAAA;AAAA,QACV;AAAA,MACF;AACA,MAAA,IAAI,KAAA,EAAO,KAAA,CAAM,CAAC,CAAA,KAAM,IAAI,CAAC,CAAA;AAAA,IAC/B,GAAG,GAAG,CAAA;AACN,IAAA,OAAO,MAAM,MAAA,CAAO,aAAA,CAAc,CAAC,CAAA;AAAA,EACrC,CAAA,EAAG,EAAE,CAAA;AAIL,EAAA,MAAM,MAA8B,EAAC;AACrC,EAAA,KAAA,MAAW,CAAC,SAAA,EAAW,EAAE,OAAO,CAAA,IAAK,QAAQ,OAAA,EAAS;AACpD,IAAA,MAAM,GAAA,GAAM,SAAA,CAAU,OAAA,CAAQ,GAAG,CAAA;AACjC,IAAA,MAAM,OAAA,GAAU,SAAA,CAAU,KAAA,CAAM,GAAA,GAAM,CAAC,CAAA;AACvC,IAAA,IAAI,CAAC,OAAA,EAAS;AACd,IAAA,MAAM,KAAA,GAAQ,MAAM,UAAA,IAAc,SAAA;AAClC,IAAA,GAAA,CAAI,OAAO,CAAA,GAAI;AAAA,MACb,KAAA;AAAA,MACA,YAAY,KAAA,GAAQ,IAAA;AAAA,MACpB,KAAA,EAAO,KAAA,CAAM,SAAA,IAAa,KAAA,CAAM,OAAA,IAAW;AAAA,KAC7C;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT","file":"chunk-E4AICMFZ.js","sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { SheetsBridgeAdapter } from \"./bridges/sheets\";\nimport { onActivity } from \"./presence/registry\";\nimport type { AgentActivityEvent } from \"./presence/types\";\n\n/**\n * Shared-session helpers for `@particle-academy/fancy-sheets`.\n *\n * fancy-sheets' `SheetWorkbook` is already controlled (`data` + `onChange`).\n * The two missing pieces for a clean shared-session experience are:\n *\n * 1. an adapter object the host can hand to {@link registerSheetsBridge}\n * without writing boilerplate, and\n * 2. a derived `CellHighlightMap` so agent edits visibly pulse on the\n * humans' screens — wired from the presence registry's per-bridge\n * activity stream.\n *\n * These are kept as host-side hooks (not part of the bridge itself) so\n * agent-integrations keeps zero hard deps on fancy-sheets. The host\n * imports SheetWorkbook directly and feeds these hooks' outputs into\n * its props.\n *\n * const wb = useSheetsAdapter(initial, { screenId: \"deal-sheet\" });\n * const highlights = useSheetsActivityHighlights({ screenId: \"deal-sheet\" });\n *\n * useEffect(() => {\n * const bridge = registerSheetsBridge(host, { adapter: wb.adapter });\n * return bridge.dispose;\n * }, [host, wb.adapter]);\n *\n * <SheetWorkbook\n * data={wb.workbook}\n * onChange={wb.setWorkbook}\n * highlights={highlights}\n * onActiveCellChange={wb.onActiveCellChange}\n * />\n */\n\n// Loose type mirror of fancy-sheets' WorkbookData — kept local so this\n// helper doesn't pull a runtime dep on the package. Apps using the helper\n// import the real `WorkbookData` from fancy-sheets and pass it through.\nexport type WorkbookLike = {\n sheets: Array<{ id: string; name: string; [k: string]: unknown }>;\n activeSheetId: string;\n};\n\nexport type SheetsAdapterOptions = {\n /** Tags the bridge's screen id so presence events route correctly. */\n screenId?: string;\n};\n\nexport type UseSheetsAdapterResult<W extends WorkbookLike> = {\n /** Controlled workbook state. Wire to `<SheetWorkbook data={…} />`. */\n workbook: W;\n /** Setter for the controlled state. Wire to `<SheetWorkbook onChange={…} />`. */\n setWorkbook: (next: W) => void;\n /** Wire to `<SheetWorkbook onActiveCellChange={…} />` to track focus. */\n onActiveCellChange: (address: string) => void;\n /** Stable adapter to hand to `registerSheetsBridge({ adapter })`. */\n adapter: SheetsBridgeAdapter;\n /** Imperative: set the active sheet + cell. Mirrors the adapter's hook. */\n setActiveCell: (sheetId: string, address: string) => void;\n /** Read-only: the address last focused (any source). */\n activeCell: string | null;\n};\n\n/**\n * useSheetsAdapter — one-liner glue between fancy-sheets' SheetWorkbook\n * and the sheets bridge.\n *\n * const wb = useSheetsAdapter(initialWorkbook, { screenId: \"...\" });\n *\n * useEffect(() => registerSheetsBridge(host, { adapter: wb.adapter }).dispose,\n * [host, wb.adapter]);\n *\n * <SheetWorkbook\n * data={wb.workbook}\n * onChange={wb.setWorkbook}\n * onActiveCellChange={wb.onActiveCellChange}\n * />\n */\nexport function useSheetsAdapter<W extends WorkbookLike>(\n initial: W,\n options: SheetsAdapterOptions = {},\n): UseSheetsAdapterResult<W> {\n const [workbook, setWorkbook] = useState<W>(initial);\n const [activeCell, setActiveCellState] = useState<string | null>(null);\n const workbookRef = useRef(workbook);\n workbookRef.current = workbook;\n\n const setActiveCell = useCallback((sheetId: string, address: string) => {\n setWorkbook((cur) => (cur.activeSheetId === sheetId ? cur : { ...cur, activeSheetId: sheetId }));\n setActiveCellState(address);\n }, []);\n\n const onActiveCellChange = useCallback((address: string) => {\n setActiveCellState(address);\n }, []);\n\n // Adapter must be stable across renders so the bridge's tool catalog\n // doesn't churn — bind it to refs that hold the latest state + setter.\n const setWorkbookRef = useRef(setWorkbook);\n setWorkbookRef.current = setWorkbook;\n\n const adapter = useMemo<SheetsBridgeAdapter>(\n () => ({\n screenId: options.screenId,\n getWorkbook: () => workbookRef.current as unknown as ReturnType<SheetsBridgeAdapter[\"getWorkbook\"]>,\n setWorkbook: (next) => setWorkbookRef.current(next as unknown as W),\n setActiveCell,\n }),\n [options.screenId, setActiveCell],\n );\n\n return {\n workbook,\n setWorkbook,\n onActiveCellChange,\n adapter,\n setActiveCell,\n activeCell,\n };\n}\n\n/**\n * Loose mirror of fancy-sheets' `CellHighlightMap`. Each key is a cell\n * address (`\"B12\"`); each value is the visual treatment to apply.\n */\nexport type SheetsCellHighlight = {\n color?: string;\n /** Background tint; if omitted, derived from `color` at low alpha. */\n background?: string;\n /** Optional label rendered in a chip on the cell. */\n label?: string;\n /** Optional className appended to the cell. */\n className?: string;\n};\n\nexport type SheetsCellHighlightMap = Record<string, SheetsCellHighlight>;\n\nexport type SheetsHighlightOptions = {\n /** Only include events for this screen (recommended). */\n screenId?: string;\n /** Highlight TTL in ms before a hit fades from the map. Default 2200. */\n ttlMs?: number;\n};\n\n/**\n * useSheetsActivityHighlights — subscribe to the presence registry,\n * produce a CellHighlightMap reflecting recent sheet-bridge activity.\n *\n * Pass the result straight into `<SheetWorkbook highlights={…} />`. Each\n * agent edit pulses in the agent's color for `ttlMs` then fades out.\n *\n * The bridge's target shape is `${sheetId}!${address}` — this hook\n * filters for the currently-active sheet and exposes only its cells.\n *\n * const highlights = useSheetsActivityHighlights({ screenId: \"deal-sheet\" });\n * <SheetWorkbook highlights={highlights} … />\n */\nexport function useSheetsActivityHighlights(\n options: SheetsHighlightOptions = {},\n): SheetsCellHighlightMap {\n const ttlMs = options.ttlMs ?? 2200;\n const screenId = options.screenId;\n const [, force] = useState(0);\n const hitsRef = useRef<\n Map<string, { event: AgentActivityEvent; expiresAt: number }>\n >(new Map());\n\n useEffect(() => {\n const off = onActivity((event) => {\n if (event.target?.kind !== \"sheet\") return;\n if (screenId && event.target.screenId && event.target.screenId !== screenId) return;\n const elementId = event.target.elementId;\n if (!elementId || !elementId.includes(\"!\")) return;\n hitsRef.current.set(elementId, { event, expiresAt: Date.now() + ttlMs });\n force((n) => n + 1);\n });\n return off;\n }, [screenId, ttlMs]);\n\n // Periodic GC — drop expired entries and force a re-render.\n useEffect(() => {\n const t = window.setInterval(() => {\n const now = Date.now();\n let dirty = false;\n for (const [k, v] of hitsRef.current) {\n if (v.expiresAt < now) {\n hitsRef.current.delete(k);\n dirty = true;\n }\n }\n if (dirty) force((n) => n + 1);\n }, 500);\n return () => window.clearInterval(t);\n }, []);\n\n // Re-derived on every render — the listener + GC timer above call\n // `force` so renders happen exactly when the map changes.\n const out: SheetsCellHighlightMap = {};\n for (const [elementId, { event }] of hitsRef.current) {\n const idx = elementId.indexOf(\"!\");\n const address = elementId.slice(idx + 1);\n if (!address) continue;\n const color = event.agentColor ?? \"#a855f7\";\n out[address] = {\n color,\n background: color + \"33\",\n label: event.agentName ?? event.agentId ?? \"agent\",\n };\n }\n return out;\n}\n"]}
|