{"_id":"@3dverse/livelink-agent","_rev":"5-a6e5f573b00f68f91c43fd80e8dc61a8","name":"@3dverse/livelink-agent","dist-tags":{"latest":"0.5.5"},"versions":{"0.5.1":{"name":"@3dverse/livelink-agent","version":"0.5.1","_id":"@3dverse/livelink-agent@0.5.1","maintainers":[{"name":"h-eal","email":"houssem@3dverse.com"},{"name":"apetitescure","email":"alex@3dverse.com"}],"dist":{"shasum":"9c5ef6ab3d71947a738d00260b1564fd0a076a2c","tarball":"https://registry.npmjs.org/@3dverse/livelink-agent/-/livelink-agent-0.5.1.tgz","fileCount":102,"integrity":"sha512-GpSb8A2KXk+m0bApShO+7czN/6Tz9Zw+JTrUBJSr6NbBSOPfJ8aA6lCAzqE8nc8pDRbVVYfDMPMGfj8YUAzGsg==","signatures":[{"sig":"MEUCIQDSXd8NC7vD+yGJEP8j0aYRszoCfma1B77vRGzbMALbMQIgTqun/jjJXRh9gI8ggUVSqWdoy/Ey4/tEpT3zy+vQ8g0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1961224},"main":"./dist/index.cjs","module":"./dist/index.mjs","exports":{".":{"types":"./dist/livelink.agent/sources/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"gitHead":"4723a3789d77f8e7407f2ef0eab3e0fd6f4161df","scripts":{"dev":"run-p dev:*","docs":"run-p typedoc typedoc:md","lint":"eslint sources samples","test":"vitest run","build":"node esbuild.js && tsc && tsc-alias","clean":"rimraf dist docs docs-md","dev:tsc":"tsc -w --noEmit","predocs":"run-p clean:docs","pretest":"node ../livelink.base/ci/auto-generate.js ../../node_modules/ ../livelink.base/ci/ ../livelink.base/_prebuild/","typedoc":"typedoc --options typedoc.config.mjs","prebuild":"node ../livelink.base/ci/auto-generate.js ../../node_modules/ ../livelink.base/ci/ ../livelink.base/_prebuild/","typecheck":"tsc --noEmit -p tsconfig.test.json","clean:docs":"rimraf docs docs-md","test:watch":"vitest","typedoc:md":"typedoc --options typedoc-md.config.mjs --plugin typedoc-plugin-no-inherit --plugin typedoc-plugin-markdown --out docs-md","dev:esbuild":"node esbuild.js dev","posttypedoc:md":"prettier --config ../../.prettierrc --write ./docs-md","typecheck:samples":"tsc --noEmit -p tsconfig.samples.json","generate:x-agent-data-ingestion":"tsx --tsconfig tsconfig.samples.json samples/playback-generators/gen-playback-x-agent-data-ingestion.ts"},"typings":"./dist/livelink.agent/sources/index.d.ts","_npmUser":{"name":"apetitescure","email":"alex@3dverse.com"},"_npmVersion":"10.9.2","description":"Headless agent package for controlling 3dverse rendering sessions programmatically (Node.js and browser compatible)","directories":{},"_nodeVersion":"22.15.0","dependencies":{"lodash":"^4.18.1","@3dverse/livelink.core":"^1.1.14"},"_hasShrinkwrap":false,"devDependencies":{"ajv":"^8.17.1","tsx":"^4.20.6","mqtt":"^5.3.6","eslint":"^10.4.0","rimraf":"^6.1.3","vitest":"^4.1.7","esbuild":"^0.28.0","globals":"^17.6.0","typedoc":"^0.28.19","prettier":"^3.8.3","tsc-alias":"^1.8.16","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^25.9.1","npm-run-all":"^4.1.5","@types/lodash":"^4.17.24","@azure/event-hubs":"^5.12.2","node-opcua-client":"^2.175.0","typescript-eslint":"^8.60.0","@vitest/coverage-v8":"^4.1.7","typedoc-plugin-mermaid":"^1.12.0","typedoc-plugin-markdown":"^4.11.0","@typescript-eslint/parser":"^8.60.0","typedoc-plugin-no-inherit":"^1.6.1","@typescript-eslint/eslint-plugin":"^8.60.0"},"peerDependencies":{"ajv":"^8.17.1","mqtt":"^5.3.6","@azure/event-hubs":"^5.12.2","node-opcua-client":"^2.175.0"},"peerDependenciesMeta":{"ajv":{"optional":true},"mqtt":{"optional":true},"@azure/event-hubs":{"optional":true},"node-opcua-client":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/livelink-agent_0.5.1_1786377289951_0.5036348348161686","host":"s3://npm-registry-packages-npm-production"}},"0.5.2":{"name":"@3dverse/livelink-agent","version":"0.5.2","_id":"@3dverse/livelink-agent@0.5.2","maintainers":[{"name":"h-eal","email":"houssem@3dverse.com"},{"name":"apetitescure","email":"alex@3dverse.com"}],"dist":{"shasum":"a094143a4eac57ca81944f633fa6149d00a89c65","tarball":"https://registry.npmjs.org/@3dverse/livelink-agent/-/livelink-agent-0.5.2.tgz","fileCount":102,"integrity":"sha512-XmloKwS3wVDwPrtnvIVCcFTpuZb3/EiUmcbit5UeU3l/myUPjYBP3+G7VzuLn7Lelhw2HRUZb7kf7ZdIehV3tA==","signatures":[{"sig":"MEQCIFsXxlLhQ6jomjTe2PNS08VRxg0K+CpzV1vFEYzpbSZFAiAVc8lEIMiakI+25x6WdcxT9kdZqgxZw7Fb2TuOy1mQkg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1961806},"main":"./dist/index.cjs","module":"./dist/index.mjs","exports":{".":{"types":"./dist/livelink.agent/sources/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"gitHead":"b1a32669899c03deece8be1f68e0e4019296087d","scripts":{"dev":"run-p dev:*","docs":"run-p typedoc typedoc:md","lint":"eslint sources samples","test":"vitest run","build":"node esbuild.js && tsc && tsc-alias","clean":"rimraf dist docs docs-md","dev:tsc":"tsc -w --noEmit","predocs":"run-p clean:docs","pretest":"node ../livelink.base/ci/auto-generate.js ../../node_modules/ ../livelink.base/ci/ ../livelink.base/_prebuild/","typedoc":"typedoc --options typedoc.config.mjs","prebuild":"node ../livelink.base/ci/auto-generate.js ../../node_modules/ ../livelink.base/ci/ ../livelink.base/_prebuild/","typecheck":"tsc --noEmit -p tsconfig.test.json","clean:docs":"rimraf docs docs-md","test:watch":"vitest","typedoc:md":"typedoc --options typedoc-md.config.mjs --plugin typedoc-plugin-no-inherit --plugin typedoc-plugin-markdown --out docs-md","dev:esbuild":"node esbuild.js dev","posttypedoc:md":"prettier --config ../../.prettierrc --write ./docs-md","typecheck:samples":"tsc --noEmit -p tsconfig.samples.json","generate:x-agent-data-ingestion":"tsx --tsconfig tsconfig.samples.json samples/playback-generators/gen-playback-x-agent-data-ingestion.ts"},"typings":"./dist/livelink.agent/sources/index.d.ts","_npmUser":{"name":"apetitescure","email":"alex@3dverse.com"},"_npmVersion":"10.9.2","description":"Headless agent package for controlling 3dverse rendering sessions programmatically (Node.js and browser compatible)","directories":{},"_nodeVersion":"22.15.0","dependencies":{"lodash":"^4.18.1","@3dverse/livelink.core":"^1.1.14"},"_hasShrinkwrap":false,"devDependencies":{"ajv":"^8.17.1","tsx":"^4.20.6","mqtt":"^5.3.6","eslint":"^10.4.0","rimraf":"^6.1.3","vitest":"^4.1.7","esbuild":"^0.28.0","globals":"^17.6.0","typedoc":"^0.28.19","prettier":"^3.8.3","tsc-alias":"^1.8.16","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^25.9.1","npm-run-all":"^4.1.5","@types/lodash":"^4.17.24","@azure/event-hubs":"^5.12.2","node-opcua-client":"^2.175.0","typescript-eslint":"^8.60.0","@vitest/coverage-v8":"^4.1.7","typedoc-plugin-mermaid":"^1.12.0","typedoc-plugin-markdown":"^4.11.0","@typescript-eslint/parser":"^8.60.0","typedoc-plugin-no-inherit":"^1.6.1","@typescript-eslint/eslint-plugin":"^8.60.0"},"peerDependencies":{"ajv":"^8.17.1","mqtt":"^5.3.6","@azure/event-hubs":"^5.12.2","node-opcua-client":"^2.175.0"},"peerDependenciesMeta":{"ajv":{"optional":true},"mqtt":{"optional":true},"@azure/event-hubs":{"optional":true},"node-opcua-client":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/livelink-agent_0.5.2_1786469455277_0.263201922596469","host":"s3://npm-registry-packages-npm-production"}},"0.5.3":{"name":"@3dverse/livelink-agent","version":"0.5.3","_id":"@3dverse/livelink-agent@0.5.3","maintainers":[{"name":"h-eal","email":"houssem@3dverse.com"},{"name":"apetitescure","email":"alex@3dverse.com"}],"dist":{"shasum":"3ed6cadee5196bb1c67db76b37715394f26f33b4","tarball":"https://registry.npmjs.org/@3dverse/livelink-agent/-/livelink-agent-0.5.3.tgz","fileCount":102,"integrity":"sha512-4aunka0A8Ut5UhHQav0/wtPq86kYj//RCSHKfkWbMgkM16wmhGOqoUyejyGsa791lYU7IxbzC9MfqGBTRhTaKQ==","signatures":[{"sig":"MEYCIQD4e63jzW3O6C6tQoq2PghpsESWLSTO+iHjgqoktKtceAIhAOqOWzM1JSAqbP9s37QWQhjqSRIe6dN4k4y7qs8NBt0W","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1961806},"main":"./dist/index.cjs","module":"./dist/index.mjs","exports":{".":{"types":"./dist/livelink.agent/sources/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"gitHead":"d9bf551b58cd1eec11622e34a3cd8f2de01d682c","scripts":{"dev":"run-p dev:*","docs":"run-p typedoc typedoc:md","lint":"eslint sources samples","test":"vitest run","build":"node esbuild.js && tsc && tsc-alias","clean":"rimraf dist docs docs-md","dev:tsc":"tsc -w --noEmit","predocs":"run-p clean:docs","pretest":"node ../livelink.base/ci/auto-generate.js ../../node_modules/ ../livelink.base/ci/ ../livelink.base/_prebuild/","typedoc":"typedoc --options typedoc.config.mjs","prebuild":"node ../livelink.base/ci/auto-generate.js ../../node_modules/ ../livelink.base/ci/ ../livelink.base/_prebuild/","typecheck":"tsc --noEmit -p tsconfig.test.json","clean:docs":"rimraf docs docs-md","test:watch":"vitest","typedoc:md":"typedoc --options typedoc-md.config.mjs --plugin typedoc-plugin-no-inherit --plugin typedoc-plugin-markdown --out docs-md","dev:esbuild":"node esbuild.js dev","posttypedoc:md":"prettier --config ../../.prettierrc --write ./docs-md","typecheck:samples":"tsc --noEmit -p tsconfig.samples.json","generate:x-agent-data-ingestion":"tsx --tsconfig tsconfig.samples.json samples/playback-generators/gen-playback-x-agent-data-ingestion.ts"},"typings":"./dist/livelink.agent/sources/index.d.ts","_npmUser":{"name":"apetitescure","email":"alex@3dverse.com"},"_npmVersion":"10.9.2","description":"Headless agent package for controlling 3dverse rendering sessions programmatically (Node.js and browser compatible)","directories":{},"_nodeVersion":"22.15.0","dependencies":{"lodash":"^4.18.1","@3dverse/livelink.core":"^1.1.14"},"_hasShrinkwrap":false,"devDependencies":{"ajv":"^8.17.1","tsx":"^4.20.6","mqtt":"^5.3.6","eslint":"^10.4.0","rimraf":"^6.1.3","vitest":"^4.1.7","esbuild":"^0.28.0","globals":"^17.6.0","typedoc":"^0.28.19","prettier":"^3.8.3","tsc-alias":"^1.8.16","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^25.9.1","npm-run-all":"^4.1.5","@types/lodash":"^4.17.24","@azure/event-hubs":"^5.12.2","node-opcua-client":"^2.175.0","typescript-eslint":"^8.60.0","@vitest/coverage-v8":"^4.1.7","typedoc-plugin-mermaid":"^1.12.0","typedoc-plugin-markdown":"^4.11.0","@typescript-eslint/parser":"^8.60.0","typedoc-plugin-no-inherit":"^1.6.1","@typescript-eslint/eslint-plugin":"^8.60.0"},"peerDependencies":{"ajv":"^8.17.1","mqtt":"^5.3.6","@azure/event-hubs":"^5.12.2","node-opcua-client":"^2.175.0"},"peerDependenciesMeta":{"ajv":{"optional":true},"mqtt":{"optional":true},"@azure/event-hubs":{"optional":true},"node-opcua-client":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/livelink-agent_0.5.3_1787865438620_0.6751155166755864","host":"s3://npm-registry-packages-npm-production"}},"0.5.4":{"name":"@3dverse/livelink-agent","version":"0.5.4","_id":"@3dverse/livelink-agent@0.5.4","maintainers":[{"name":"h-eal","email":"houssem@3dverse.com"},{"name":"apetitescure","email":"alex@3dverse.com"}],"dist":{"shasum":"fff5148cf675775e4cf3c19b3dd23ad8e3502cf5","tarball":"https://registry.npmjs.org/@3dverse/livelink-agent/-/livelink-agent-0.5.4.tgz","fileCount":106,"integrity":"sha512-Su6/FWBycXUrReFBldGBEfV9uh6zX2QRidN3QwGgC/B5Vq9awRxUcW9a7QjqzCxmPoUCxtbhTCs3TTRpzC27XA==","signatures":[{"sig":"MEYCIQDIG5lq3jsKdkZaQRIkGo3UtggSnT59odzybcSrTK7oaAIhAKdLejC1CVaZZ4au0gmAjWYGGGXOEoI08QHu50p64piW","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":2147823},"main":"./dist/index.cjs","module":"./dist/index.mjs","exports":{".":{"types":"./dist/livelink.agent/sources/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"gitHead":"8dc92021772dcaa2d32884e98a454a86d93ad82a","scripts":{"dev":"run-p dev:*","docs":"run-p typedoc typedoc:md","lint":"eslint sources","test":"vitest run","build":"node esbuild.js && tsc && tsc-alias","clean":"rimraf dist docs docs-md","dev:tsc":"tsc -w --noEmit","predocs":"run-p clean:docs","pretest":"node ../livelink.base/ci/auto-generate.js ../../node_modules/ ../livelink.base/ci/ ../livelink.base/_prebuild/","typedoc":"typedoc --options typedoc.config.mjs","prebuild":"node ../livelink.base/ci/auto-generate.js ../../node_modules/ ../livelink.base/ci/ ../livelink.base/_prebuild/","typecheck":"tsc --noEmit -p tsconfig.test.json","clean:docs":"rimraf docs docs-md","test:watch":"vitest","typedoc:md":"typedoc --options typedoc-md.config.mjs --plugin typedoc-plugin-no-inherit --plugin typedoc-plugin-markdown --out docs-md","dev:esbuild":"node esbuild.js dev","posttypedoc:md":"prettier --config ../../.prettierrc --write ./docs-md"},"typings":"./dist/livelink.agent/sources/index.d.ts","_npmUser":{"name":"apetitescure","email":"alex@3dverse.com"},"_npmVersion":"10.9.2","description":"Headless agent package for controlling 3dverse rendering sessions programmatically (Node.js and browser compatible)","directories":{},"_nodeVersion":"22.15.0","dependencies":{"lodash":"^4.18.1","@3dverse/livelink.core":"^1.1.14"},"_hasShrinkwrap":false,"devDependencies":{"ajv":"^8.17.1","tsx":"^4.20.6","mqtt":"^5.3.6","eslint":"^10.4.0","rimraf":"^6.1.3","vitest":"^4.1.7","esbuild":"^0.28.0","globals":"^17.6.0","typedoc":"^0.28.19","prettier":"^3.8.3","tsc-alias":"^1.8.16","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^25.9.1","npm-run-all":"^4.1.5","@types/lodash":"^4.17.24","@azure/event-hubs":"^5.12.2","node-opcua-client":"^2.175.0","typescript-eslint":"^8.60.0","@vitest/coverage-v8":"^4.1.7","typedoc-plugin-mermaid":"^1.12.0","typedoc-plugin-markdown":"^4.11.0","@typescript-eslint/parser":"^8.60.0","typedoc-plugin-no-inherit":"^1.6.1","@typescript-eslint/eslint-plugin":"^8.60.0"},"peerDependencies":{"ajv":"^8.17.1","mqtt":"^5.3.6","@azure/event-hubs":"^5.12.2","node-opcua-client":"^2.175.0"},"peerDependenciesMeta":{"ajv":{"optional":true},"mqtt":{"optional":true},"@azure/event-hubs":{"optional":true},"node-opcua-client":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/livelink-agent_0.5.4_1787957563387_0.5078628448501408","host":"s3://npm-registry-packages-npm-production"}},"0.5.5":{"name":"@3dverse/livelink-agent","version":"0.5.5","description":"Headless agent package for controlling 3dverse rendering sessions programmatically (Node.js and browser compatible)","main":"./dist/index.cjs","module":"./dist/index.mjs","typings":"./dist/livelink.agent/sources/index.d.ts","exports":{".":{"types":"./dist/livelink.agent/sources/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"scripts":{"prebuild":"node ../livelink.base/ci/auto-generate.js ../../node_modules/ ../livelink.base/ci/ ../livelink.base/_prebuild/","build":"node esbuild.js && tsc && tsc-alias","typecheck":"tsc --noEmit -p tsconfig.test.json","lint":"eslint sources","clean":"rimraf dist docs docs-md","clean:docs":"rimraf docs docs-md","dev":"run-p dev:*","dev:esbuild":"node esbuild.js dev","dev:tsc":"tsc -w --noEmit","predocs":"run-p clean:docs","docs":"run-p typedoc typedoc:md","pretest":"node ../livelink.base/ci/auto-generate.js ../../node_modules/ ../livelink.base/ci/ ../livelink.base/_prebuild/","test":"vitest run","test:watch":"vitest","typedoc":"typedoc --options typedoc.config.mjs","typedoc:md":"typedoc --options typedoc-md.config.mjs --plugin typedoc-plugin-no-inherit --plugin typedoc-plugin-markdown --out docs-md","posttypedoc:md":"prettier --config ../../.prettierrc --write ./docs-md"},"dependencies":{"@3dverse/livelink.core":"^1.1.14","lodash":"^4.18.1"},"peerDependencies":{"@azure/event-hubs":"^5.12.2","ajv":"^8.17.1","mqtt":"^5.3.6","node-opcua-client":"^2.175.0"},"peerDependenciesMeta":{"@azure/event-hubs":{"optional":true},"ajv":{"optional":true},"mqtt":{"optional":true},"node-opcua-client":{"optional":true}},"devDependencies":{"@azure/event-hubs":"^5.12.2","@eslint/js":"^10.0.1","@types/lodash":"^4.17.24","@types/node":"^25.9.1","@typescript-eslint/eslint-plugin":"^8.60.0","@typescript-eslint/parser":"^8.60.0","@vitest/coverage-v8":"^4.1.7","ajv":"^8.17.1","esbuild":"^0.28.0","eslint":"^10.4.0","globals":"^17.6.0","mqtt":"^5.3.6","node-opcua-client":"^2.175.0","npm-run-all":"^4.1.5","prettier":"^3.8.3","rimraf":"^6.1.3","tsc-alias":"^1.8.16","tsx":"^4.20.6","typedoc":"^0.28.19","typedoc-plugin-markdown":"^4.11.0","typedoc-plugin-mermaid":"^1.12.0","typedoc-plugin-no-inherit":"^1.6.1","typescript":"^6.0.3","typescript-eslint":"^8.60.0","vitest":"^4.1.7"},"gitHead":"5ca9722f3e93b8bd564114afa1b85834708b4109","_id":"@3dverse/livelink-agent@0.5.5","_nodeVersion":"24.20.0","_npmVersion":"11.19.0","dist":{"integrity":"sha512-a3Z4a5AissgBYIdq0F7LK6lfdGBlWJW88bwNeM1Zqc50b8VgIJ2BwOPkOinGiHpDtgD8oJyWo5TVDuOqxFA+jQ==","shasum":"3fe491c33e353f6e1ec197184936565b653b7596","tarball":"https://registry.npmjs.org/@3dverse/livelink-agent/-/livelink-agent-0.5.5.tgz","fileCount":106,"unpackedSize":2148162,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDTfonmgZ3oHkebk/ir8/O1hT9lRg8CJWZ0LIxhi0US4AIhAKCt7iLhI1OUNOkO1vSyHZCEorX6/j6VzrUKpfBFEGBE"}]},"_npmUser":{"name":"GitLab CI/CD","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"gitlab","oidcConfigId":"oidc:d9e377cf-45bc-4dd8-9cb0-041f28d5423c"}},"directories":{},"maintainers":[{"name":"h-eal","email":"houssem@3dverse.com"},{"name":"apetitescure","email":"alex@3dverse.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/livelink-agent_0.5.5_1788442023506_0.3662002072465371"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-10T15:54:49.779Z","modified":"2026-09-03T13:27:03.872Z","0.5.1":"2026-08-10T15:54:50.109Z","0.5.2":"2026-08-11T17:30:55.478Z","0.5.3":"2026-08-27T21:17:18.795Z","0.5.4":"2026-08-28T22:52:43.539Z","0.5.5":"2026-09-03T13:27:03.683Z"},"description":"Headless agent package for controlling 3dverse rendering sessions programmatically (Node.js and browser compatible)","maintainers":[{"name":"h-eal","email":"houssem@3dverse.com"},{"name":"apetitescure","email":"alex@3dverse.com"}],"readme":"# @3dverse/livelink.agent\n\n## About\n\nHeadless agent package for controlling 3dverse rendering sessions programmatically (Node.js and browser compatible).\n\nAn agent is a headless client that attaches to one or more sessions of a scene and controls them through the entity API: create, update and delete entities, and react to changes made by other clients. Typical use case: bridging an external datasource (MQTT broker, OPC-UA server, WebSocket feed...) to live 3dverse scenes.\n\nSee the official documentation: [Connect Live Data](https://docs.3dverse.com/connect-live-data)\n\n## Samples\n\n**Try it**: [Agent Data Ingestion Demo](https://samples.livelink.3dverse.com/#/agent-data-ingestion)\n\nRunnable examples:\n\n- **Node.js agents** (MQTT and OPC UA): [`livelink.samples/src/samples/agent/`](https://github.com/3dverse/livelink/tree/release/livelink.samples/src/samples/agent/README.md) — headless scripts that drive a scene from live infrastructure you start yourself with one docker command. `opcua-ingestion/` drives a machine cell from Microsoft's simulated PLC, [iot-edge-opc-plc](https://github.com/Azure-Samples/iot-edge-opc-plc), and `mqtt-ingestion/` drives a plant floor from a broker fed by [mqtt-sim](https://github.com/marcelo-6/mqtt-sim).\n- **Browser samples**: [`x-agent-data-ingestion/`](https://samples.livelink.3dverse.com/#/agent-data-ingestion/) — replays recorded event streams using the `playback` transport, and [`x-agent-multiplayer-game/`](https://samples.livelink.3dverse.com/#/agent-multiplayer-game/) — demonstrates multi-session agent coordination.\n\n## Installation\n\n```bash\nnpm install @3dverse/livelink-agent\n```\n\n## Usage\n\nFor the common case — \"an external event stream drives entities in the scene\" — the package ships an\n**opt-in ingestion layer**. Its heavy dependencies (`mqtt`, `@azure/event-hubs`, `node-opcua-client`,\n`ajv`) are optional peers, loaded lazily by the transport or validator that needs them; not\ninstalling them is fine, and costs nothing at build or run time.\n\nTwo objects. An **`EventMapping`** — a plain object — says how one event type drives entities. An\n**`IngestionPipeline`** runs the mappings against every scene bound to it, and `ingest` is how events\nget in:\n\n```typescript\nimport { IngestionPipeline, type EventMapping } from \"@3dverse/livelink-agent\";\n\nconst mapping: EventMapping = {\n  // Optional selectors: an MQTT-style pattern over the event's channel, and/or a payload predicate.\n  channel: \"devices/+/telemetry\",\n  // Optional JSON Schema: the FIRST matching event is validated against it (every event with the\n  // pipeline's `validate: true` — a debugging tool).\n  schema: { type: \"object\", properties: { pos: { type: \"array\" } }, required: [\"pos\"] },\n  // Which scene entities the ids these events carry address — one of four strategies, detailed below.\n  entities: {\n    spawn: {\n      name: \"device-{id}\",\n      components: { local_transform: { position: [0, 0, 0] } },\n      options: { delete_on_client_disconnection: true },\n    },\n  },\n  // What one event does. Return one `{ id, update }`, an array of them when a single event carries\n  // several objects, or null to ignore the event. `update` is a set of component patches,\n  // \"delete\" / \"hide\" / \"show\" to act on the entity as a whole, or `continuous(...)` for\n  // something that keeps moving between events (see below).\n  updates: event => ({\n    id: event.channel.split(\"/\")[1], // the id can come from the channel, the payload, anywhere\n    update: { local_transform: { position: (event.payload as { pos: [number, number, number] }).pos } },\n  }),\n};\n\nconst pipeline = new IngestionPipeline({ mappings: mapping }); // or an array\npipeline.bind({ scene });\nawait pipeline.ingest({ channel: \"devices/42/telemetry\", payload: { pos: [1, 2, 3] } });\n```\n\nNothing above needs an agent, a session or a broker — which is what makes a mapping straightforward\nto unit-test, to drive from a webhook or a REST handler, and to replay one frame at a time.\n\n### Updates that keep going\n\nSome events carry a **rate**, not a value — \"the shaft is turning at 90 rpm\". A rate still means\nsomething after the message that delivered it, so writing a finished patch would leave the entity\nfrozen until the next message, which for a machine reporting only on change may be minutes away.\n\nWrap the update in `continuous()` and it keeps producing values until a later event for that id\nreplaces it:\n\n```typescript\nupdates: event => {\n  const { rpm } = event.payload as { rpm: number };\n  const id = event.channel.split(\"/\")[1];\n\n  return {\n    id,\n    update: continuous<{ angle_deg: number }>(\n      ({ delta_seconds, state }) => {\n        // 360 degrees a turn, 60 seconds a minute.\n        state.angle_deg = (state.angle_deg + rpm * 6 * delta_seconds) % 360;\n        return {\n          local_transform: {\n            eulerOrientation: [state.angle_deg, 0, 0]\n          }\n        };\n      },\n      { initial_state: { angle_deg: 0 } },\n    ),\n  };\n},\n```\n\nThe sample is handed three things:\n\n|                 |                                                                                           |\n| --------------- | ----------------------------------------------------------------------------------------- |\n| `delta_seconds` | Since the previous sample, `0` on the installing event. For a value that **accumulates**. |\n| `since_seconds` | Since this motion started. For a value that is a closed form of its age — a fade, a ramp. |\n| `state`         | Scratch space belonging to the **entity**, not to this motion.                            |\n\n`state` survives the event that replaces the continuation, so a new rpm picks the shaft up where it\nstands rather than snapping it back; `initial_state` therefore applies only the first time an entity is\ngiven one.\n\nA motion is installed per `(mapping, id)` — not per channel, and not per session, so two sessions on the\nsame scene turn the same shaft at the same speed. It runs until one of five things happens: the sample\nreturns `null`, it throws, a later event for that id replaces it, it or an event produces `\"hide\"` or\n`\"delete\"`, or you call `pipeline.clearContinuations()`.\n\nNothing expires a motion on a timer, so a stream that dies leaves the scene moving: watch\n`last_event_at` against `continuations_active` (below) for that, and `SceneIngestion` calls\n`clearContinuations()` when it stops its sources.\n\n> **`updates` returning `null` does not stop a motion.** It means the message said nothing about that\n> entity, which is what lets one topic carry payloads of several shapes. To stop a motion from an event,\n> return `continuous(() => null)`. A plain component patch does not stop one either — patch and motion\n> are independent writes, so on a shared component the tick wins.\n\n`SceneIngestion` runs the clock (`ticksPerSecond`, twice the client's flush rate by default, `0` to\nswitch it off), redundant writes are still deduplicated so an entity holding still costs nothing, and\nticks are **not** counted as events. Driving the pipeline yourself, `pipeline.tick(elapsed_seconds)` owns\nno timer, so a motion replays exactly from a test:\n`await pipeline.ingest(event); await pipeline.tick(0.5);`.\n\n### Addressing entities: the four strategies\n\n| Strategy                                               | How the id finds its entity                                                                                                                      | Linkage                                              | Typical use                                                                      |\n| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- | -------------------------------------------------------------------------------- |\n| `byName: \"{id}\"` (string or `({id, event}) => string`) | Looks up an entity **already in the scene** by name (`{id}` substituted, or computed)                                                            | Optional                                             | The scene is already named after the stream's ids — no UUID configuration needed |\n| `byUuid: { \"servo-01\": \"<uuid>\" }`                     | A fixed, closed id → UUID table, looked up in the scene                                                                                          | Optional (per entry, via `{ entity_uuid, linkage }`) | A small, known population (e.g. named parts of one machine)                      |\n| `resolve: ({ id, event }) => ...`                      | An arbitrary function returning a UUID, `{ entity_uuid, linkage }`, or `null` — still resolves an **existing** entity, just computed dynamically | Optional                                             | An external lookup service, or a naming convention with exceptions               |\n| `spawn: { name, components, options }`                 | No pre-existing entity — one is **created** per new id, from a template, the first time that id is seen                                          | Not accepted — always created at the scene root      | New objects arriving over the stream (a device joining a fleet)                  |\n\nThe four are mutually exclusive — set exactly one per mapping (enforced by TypeScript's discriminated\nunion; a mapping setting none throws at construction). Resolution is cached per id per scene: once an\nid resolves (or fails to), later events for that id reuse the result instead of re-running the\nstrategy. A cached failure is retried automatically when the scene's own\n`on-entities-created`/`on-entities-deleted` events suggest the answer changed. `spawn` follows the\nsame cache: the entity it creates is reused on every later event for that id, and only a fresh `spawn`\nhappens after that entity is gone (deleted by this mapping's `\"delete\"` directive, or by another\nclient) and a further event arrives for the same id. An id that resolves to nothing surfaces as\n`unresolved_entity` in `ingestion.stats.drops` (below).\n\n### Binding it to the sessions of an agent\n\n`SceneIngestion` is the wiring: it binds each ready session's scene to the pipeline, unbinds it when\nthe session goes, and starts the data sources on the first binding.\n\n```typescript\nimport { Agent, SceneIngestion } from \"@3dverse/livelink-agent\";\n\nconst ingestion = new SceneIngestion({\n  agent: new Agent({ config: { scene_id, token } }),\n  pipeline, // always yours to build — see above\n  sources: [{ kind: \"mqtt\", config: { broker_url, topics: [\"devices/#\"] } }],\n});\n\ningestion.addEventListener(\"on-error\", ({ error }) => report(error));\n\nawait ingestion.start();\n// ...events flow. You can push your own in at any time:\nawait ingestion.ingest({ channel: \"devices/42/telemetry\", payload });\nawait ingestion.stop();\n```\n\nErrors split by layer: the **pipeline**'s `onError` reports a mapping that throws or an entity write\nthat fails; the **ingestion**'s `on-error` event reports a source that fails to start or an underlying\nagent error. Point both at the same handler for a single channel. An `on-error` nobody listens for\nfalls back to the console rather than vanishing.\n\n`sources` is optional, and `SceneIngestion` is itself an `EventSink` — so a transport you own can be\npointed straight at it, and events from anywhere else can be pushed in with `ingest`.\n\nSources start **lazily, on the first ready session**, and are shared by every session. Until a\nsession is ready nothing is subscribed; anything ingested with no scene bound is counted as a\n`no_binding` drop rather than lost silently.\n\n### Knowing whether data is flowing\n\n```typescript\nconst { events_received, updates_applied, components_written, drops, last_event_at } = ingestion.stats!;\n```\n\n`drops` breaks down by reason — `no_binding`, `no_mapping_matched`, `schema`, `no_id`, `no_updates`,\n`unresolved_entity` — which is what separates \"the stream isn't arriving\" from \"it arrives but no\nmapping wants it\" during bring-up. `per_mapping` carries the same counters mapping by mapping, plus\n`continuations_active`: with several mappings, that is what says which one is still driving something\nafter its stream went quiet.\n\nCounting is on by default: it allocates nothing and costs a few integer increments per event, well\nunder a single component write. For the very highest-frequency streams, build the pipeline with\n`stats: false` — `stats` then reports `null` rather than zeros, so a disabled counter can never be\nmisread as \"nothing flowed\".\n\n`SceneIngestion` also emits `on-running`, `on-session-bound` /\n`on-session-unbound`, `on-error`, and re-emits the agent's own session events, so you never have to\nreach through `.agent` to observe it.\n\n### Transports\n\nThe bundled transports are `mqtt`, `opcua`, `azure-event-hub` and `playback`. **Note that `channel`\nonly means something on some of them**: it is the topic on MQTT, the node id — or its alias — on OPC\nUA, the recorded topic on playback, and the _partition id_ — a load-balancing artifact — on Azure\nEvent Hubs, where you should select on the payload with `when` instead. See\n[ARCHITECTURE.md](./ARCHITECTURE.md) for the full picture.\n\n`opcua` subscribes to variables on an OPC UA server over `opc.tcp://` — the classic client/server\nprofile a PLC exposes — and publishes each value change as `{ node_id, value, status }`. Raw node ids\nmake unreadable channels, so alias them:\n\n```typescript\nsources: [\n  {\n    kind: \"opcua\",\n    config: {\n      endpoint_url: \"opc.tcp://plc.example.com:4840\",\n      nodes: [{ node_id: 'ns=3;s=\"DB_Line1\".\"Temperature\"', channel: \"plc/line1/temperature\" }],\n      publishing_interval: 500,\n      security_mode: \"SignAndEncrypt\", // implies Basic256Sha256; omit for an unsecured endpoint\n      username: \"operator\",\n      password: process.env.PLC_PASSWORD,\n    },\n  },\n];\n```\n\nNode.js only, `opc.tcp` being raw TCP. Samples whose status is Bad carry no meaningful value and are\ndropped rather than written into the scene, logging once per node instead of at every publication.\nThe intervals are a _request_ — the server answers with the pace it will hold to, and most refuse to\npublish faster than 50 ms — so a request revised upward is logged once too, rather than leaving a\nscene moving at half the configured rate with nothing to say why.\nA secured connection makes node-opcua generate a self-signed client certificate on first use, which\nthe server has to be told to trust — on a Siemens PLC that is a manual step in its web interface,\nand the failure everyone hits first.\n\n**If the plant already bridges OPC UA to MQTT** — OPC UA PubSub over MQTT on recent firmware,\nTelegraf's `inputs.opcua`, Kepware, Ignition — point `mqtt` at that broker instead: one hop fewer,\nand no OPC UA session to keep alive next to the scene. `opcua` is for the servers that offer no such\nbridge.\n\n`playback` replays a recorded event stream from wherever it lives — a file, a URL, a string, bytes,\na stream, or records you parsed yourself — so the same mapping can be brought up against a dump\nbefore it ever meets a broker, in Node or in the browser:\n\n```typescript\nsources: [{ kind: \"playback\", config: { source: { url: \"/recordings/devices.json\" }, speed: 2 } }];\n```\n\nOnly `source: { file_path }` needs Node. `speed` scales the recorded pace (default `1`, real time)\nand `loop` (default `true`) starts the recording over when it ends. The former `file` kind is a\ndeprecated alias.\n\nFor runnable examples, see the [Samples](#samples) section above.\n\nNeed the raw `Agent` API instead — no ingestion layer? See\n[Using the agent directly](#using-the-agent-directly).\n\n## Session modes\n\nThe `mode` option of the agent config selects how the agent attaches to sessions:\n\n| Mode                        | Behavior                                                                                                                          |\n| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |\n| `\"join-or-start\"` (default) | Join an existing session, or create one if none exists.                                                                           |\n| `\"start\"`                   | Always create a new session.                                                                                                      |\n| `\"join\"`                    | Join a single existing session. Fails if none exists, unless `watch` is enabled, in which case the agent idles until one appears. |\n| `\"join-all\"`                | Join all existing sessions of the scene.                                                                                          |\n| `\"manual\"`                  | attach to nothing on start; the agent stays idle and joins sessions on demand through its `join` method.                          |\n\nAn agent that loses its last session without being told to — it dropped, or the leave condition\nfired — has no way of getting more work, so it stops itself and dispatches `on-stopped`. A\n`SceneIngestion` stops its sources with it, which is what lets a single-session agent's process exit\non its own. This does not apply while `watch` is enabled, in `\"manual\"` mode, or to a session left\ndeliberately through `leave()` — that one can be rejoined with `join()`.\n\n### Watching for sessions\n\nWith the `watch` option (valid in `\"join\"` and `\"join-all\"` modes), the agent polls the session list and joins sessions as they appear:\n\n```typescript\nconst agent = new Agent({\n  config: {\n    scene_id,\n    token,\n    mode: \"join-all\",\n    watch: { interval_seconds: 10 },\n  },\n});\nawait agent.start();\n```\n\nIn `\"join\"` mode, the watch loop only joins a session when the agent is not attached to one (this also serves as a reconnect mechanism after a connection loss).\n\n### Leaving on condition\n\nWith the `leave_on_condition` option, the agent leaves any session if a condition is not met for the given duration:\n\n```typescript\nconst agent = new Agent({\n  config: {\n    scene_id,\n    token,\n    mode: \"join-all\",\n    watch: { interval_seconds: 10 },\n    leave_on_condition: { after_seconds: 60 },\n  },\n});\nawait agent.start();\n```\n\nBy default, the agent stays while any other client is connected. To make it ignore other agents — so a session occupied only by agents still closes — give it an `agent_roster`: a well-known entity under which each agent registers a marker entity named after its client id (created automatically on join, with `delete_on_client_disconnection`). When `agent_roster_id` is set, the agent stays only while a client without a matching marker (a real viewer) is present, instead of relying on the unreliable `is_headless` flag. If the entity is not found in the scene, an error is logged and the check falls back to plain other-client presence.\n\n```typescript\nleave_on_condition: {\n    after_seconds: 60,\n    agent_roster_id: \"d577efd3-cca8-41ca-a58e-27e944f7b5de\",\n}\n```\n\nCustomize the decision with the `should_stay` predicate, which receives the session's livelink and the other clients:\n\n```typescript\nleave_on_condition: {\n    after_seconds: 60,\n    should_stay: ({ livelink, other_clients }) => other_clients.some(c => c.client_type === \"user\"),\n}\n```\n\nNote that a `Client` seen by an agent has **identity only** — `id`, `user_id`, `username`, `client_type`, `is_external`. What a client shows (the camera entities it views the scene through, the entity under its mouse pointer) travels in the client metadata piggybacked on the video frames, which an agent never receives; those members live on the browser SDK's `Client`. To reach a viewer's own entities from an agent, have the viewer publish what it wants the agent to know into the scene, or key the state you keep on `client.id`.\n\nA session left on condition is only rejoined by the watch loop once the condition is met again. A session left deliberately (`agent.leave()` or `agent.stop()`) is never rejoined.\n\nNote: right after joining a session, the client list is populated asynchronously. The leave timer may arm immediately and is simply cleared as soon as the condition is met — with timeouts in seconds this is harmless.\n\n## Lifecycle events\n\nA single `Agent` instance can be attached to several sessions at once, and dispatches its lifecycle events on itself. Each session event carries its `Livelink` directly as `event.livelink`. To keep per-session state, hold your own map keyed by `event.livelink.session.session_id` and clean it up on `on-session-left`.\n\n```typescript\n// The agent created this session: seed the scene state.\nagent.addEventListener(\"on-session-created\", event => {});\n\n// The agent joined a pre-existing session.\nagent.addEventListener(\"on-session-joined\", event => {});\n\n// Always emitted, after on-session-created or on-session-joined.\nagent.addEventListener(\"on-session-ready\", event => {});\n\n// The agent left a session. event.reason is \"left-on-condition\" | \"disconnected\" | \"stopped\".\nagent.addEventListener(\"on-session-left\", event => console.log(\"left\", event.reason));\n\n// An error occurred (failed join, failed session list poll...). event.livelink is null\n// when the error is not tied to an established session.\nagent.addEventListener(\"on-error\", event => console.error(event.error));\n```\n\nThere is no \"stopped\" event: `agent.stop()` is something you call, so do any post-stop cleanup after `await agent.stop()` returns.\n\nNote that `on-session-ready` does **not** mean every entity of the scene is addressable: a scene pulling others in through `scene_ref` components is streamed in progressively, and entities living in those referenced scenes do not exist server-side until the server says it is done. `await livelink.scene.waitForSceneLoaded()` before looking one up — the ingestion layer above already does it for you, before resolving anything. It resolves `true` once the scenes are loaded and `false` if the session disconnected first, and never throws. Note that it has **no timeout**: a scene whose reference the token cannot read never finishes loading, and the wait then only ends on disconnection, so race it against a deadline of your own if you cannot afford to be parked.\n\n## Using the agent directly\n\nThis section covers the `Agent` API directly, without the ingestion layer above — reach for it when a\nmapping doesn't fit, or you already have an ingestion layer of your own.\n\n### Basic usage\n\n```typescript\nimport { Agent } from \"@3dverse/livelink-agent\";\n\nconst agent = new Agent({\n  config: {\n    scene_id: \"your-scene-id\",\n    token: \"your-token\",\n  },\n});\n\nagent.addEventListener(\"on-session-ready\", async event => {\n  const entity = await event.livelink.scene.findEntity({ entity_uuid: \"...\" });\n  entity?.updateComponent(\"local_transform\", { position: [0, 1, 0] });\n});\n\nawait agent.start();\n```\n\nBy default the agent joins an existing session of the scene, or creates a new one if none exists (`\"join-or-start\"` mode).\n\n### auto_broadcast\n\nFor a smooth real time animation, you want to set `Entity.auto_broadcast` to `false` e.g `entity.auto_broadcast = false` in the upper code snippet before starting to animate the entity. Or, if you create the entity from the agent, you can set it in the options:\n\n```typescript\nimport { Agent } from \"@3dverse/livelink-agent\";\n\nconst agent = new Agent({\n  config: {\n    scene_id: \"your-scene-id\",\n    token: \"your-token\",\n  },\n});\n\nagent.addEventListener(\"on-session-ready\", async event => {\n  const entity = await event.livelink.scene.newEntity({\n    name: `Animated object`,\n    components: {\n      local_transform: \"default\",\n    },\n    options: {\n      auto_broadcast: false,\n      delete_on_client_disconnection: true,\n    },\n  });\n});\n\nawait agent.start();\n```\n\nThe ingestion layer's `IngestionPipeline` already does this automatically for every entity it resolves\nor spawns (`manage_auto_broadcast: true` by default) — set `manage_auto_broadcast: false` on the\npipeline to manage it yourself instead.\n\n### Wiring a datasource by hand\n\nThe ingestion layer above is the shortest path. Underneath it, the package stays datasource-agnostic:\nthe entity API and the typed events are the integration surface, and you can wire any messaging\nsystem (MQTT, a WebSocket feed, a serial bus...) against a raw agent yourself:\n\n```typescript\nimport { Agent, Livelink, EntityUpdatedEvent } from \"@3dverse/livelink-agent\";\nimport mqtt from \"mqtt\"; // or any other client\n\nconst agent = new Agent({ config: { scene_id, token } });\nconst client = mqtt.connect(\"mqtt://broker.example.com\");\n\nasync function applyUpdate(livelink: Livelink, entity_uuid: string, position: [number, number, number]) {\n  const entity = await livelink.scene.findEntity({ entity_uuid });\n  // For smooth animation, do not broadcast the entity's transform to other clients.\n  entity.auto_broadcast = false;\n  entity?.updateComponent(\"local_transform\", { position });\n}\n\n// Inbound: datasource messages -> entity updates, in every attached session.\nclient.on(\"message\", (topic, payload) => {\n  const { entity_uuid, position } = JSON.parse(payload.toString());\n  for (const livelink of agent.livelinks) {\n    void applyUpdate(livelink, entity_uuid, position);\n  }\n});\nclient.subscribe(\"devices/+/position\");\n\nagent.addEventListener(\"on-session-ready\", async event => {\n  // Outbound: entity changes made by other clients -> datasource messages.\n  const entity = await event.livelink.scene.findEntity({ entity_uuid: \"...\" });\n  entity?.addEventListener(\"on-entity-updated\", (event: EntityUpdatedEvent) => {\n    if (event.isExternal()) {\n      client.publish(\"scene/updates\", JSON.stringify(entity.local_transform));\n    }\n  });\n});\n\nawait agent.start();\n\n// ...later, on shutdown: stop the agent, then close the datasource once it has\n// left every session.\nawait agent.stop();\nclient.end();\n```\n\nUpdates made through `updateComponent()` are batched and sent to the server automatically (30 updates/s, persisted once per second by default — tune with the `headless_client` option of the agent config).\n\n## Restarting an agent\n\n`agent.stop()` leaves every session and halts the watch loop, but the agent object stays usable: `agent.start()` attaches a fresh wave using the same config, and listeners added with `agent.addEventListener` survive the cycle. Starting an already-started agent throws.\n","readmeFilename":"README.md"}