10 KiB
@elizaos/capacitor-contacts
Capacitor plugin that exposes Android's ContactsContract to an Eliza agent's JavaScript/TypeScript runtime, with an explicit web fallback.
Purpose / role
This is a Capacitor plugin (not an elizaOS Plugin object). It does not register elizaOS actions, providers, or evaluators directly. Instead it exposes a typed JS bridge (Contacts) that elizaOS actions in other packages can call to read, create, and import contacts on Android. On web/node the bridge returns empty results or throws for write operations.
The plugin is opt-in: it must be registered with Capacitor in the host Android app and imported explicitly by any elizaOS action that needs it.
Plugin surface
This is a Capacitor bridge plugin, not an elizaOS plugin. It exposes one global object:
| Export | Description |
|---|---|
Contacts |
Registered Capacitor plugin instance (ElizaContacts bridge) |
ContactsPlugin |
TypeScript interface for the three bridge methods |
ContactSummary |
Type for a returned contact record |
ListContactsOptions |
Options for listContacts |
CreateContactOptions |
Options for createContact |
ImportVCardOptions |
Options for importVCard |
ImportedContactSummary |
Extended ContactSummary with sourceName |
Bridge methods
| Method | Platform | Notes |
|---|---|---|
listContacts(options?) |
Android | Requires READ_CONTACTS. Optional query (case-insensitive search across name/phone/email) and limit (1–500, default 100). Returns { contacts: ContactSummary[] }. |
createContact(options) |
Android | Requires WRITE_CONTACTS. displayName required; accepts phoneNumber/phoneNumbers and emailAddress/emailAddresses. Returns { id: string }. |
importVCard(options) |
Android | Requires WRITE_CONTACTS. Parses RFC 6350 vCard text (handles line folding, FN/N/TEL/EMAIL fields, \-escapes). Returns { imported: ImportedContactSummary[] }. |
Web fallback (ContactsWeb): listContacts returns { contacts: [] }, createContact/importVCard throw.
Layout
plugins/plugin-native-contacts/
src/
index.ts — registerPlugin("ElizaContacts") + re-exports everything from definitions
definitions.ts — all TypeScript interfaces (ContactSummary, ContactsPlugin, …)
web.ts — ContactsWeb (web fallback: listContacts=[], writes throw)
android/
src/main/
AndroidManifest.xml — READ_CONTACTS + WRITE_CONTACTS permissions
java/ai/eliza/plugins/contacts/
ContactsPlugin.kt — full Kotlin implementation: listContacts, createContact, importVCard, vCard parser
build.gradle
rollup.config.mjs — bundles dist/esm → dist/plugin.js (IIFE) + dist/plugin.cjs.js
tsconfig.json
package.json
Commands
Scripts are defined in package.json; run them from the repo root with bun run --cwd:
bun run --cwd plugins/plugin-native-contacts clean # remove build output
bun run --cwd plugins/plugin-native-contacts build # build package artifacts
bun run --cwd plugins/plugin-native-contacts typecheck # TypeScript typecheck
bun run --cwd plugins/plugin-native-contacts lint # mutating Biome check
bun run --cwd plugins/plugin-native-contacts lint:check # read-only Biome check
bun run --cwd plugins/plugin-native-contacts format # write formatting
bun run --cwd plugins/plugin-native-contacts format:check # read-only formatting check
bun run --cwd plugins/plugin-native-contacts test # run package tests
bun run --cwd plugins/plugin-native-contacts prepublishOnly # publish-time build hook
bun run --cwd plugins/plugin-native-contacts build:unlocked # bun run clean && tsc && bunx rollup -c rollup.config.mjs
Config / env vars
None. This plugin requires no env vars. Android runtime permissions (READ_CONTACTS, WRITE_CONTACTS) are declared in the plugin's AndroidManifest.xml and merged by the host app's build system. The host app must grant them at runtime before calling bridge methods.
How to extend
Add a new bridge method
- Add the method signature to
src/definitions.tsinContactsPlugin. - Implement the web fallback in
src/web.ts(ContactsWeb). - Implement the real method in
android/src/main/java/ai/eliza/plugins/contacts/ContactsPlugin.kt— annotate with@PluginMethod, check permissions withhasPermission(Manifest.permission.*), resolve or reject thePluginCall. - Run
bun run --cwd plugins/plugin-native-contacts buildto regeneratedist/. - Rebuild the host Android app so the new method is available in the webview bridge.
Add a new type
Add the interface/type to src/definitions.ts and re-export via src/index.ts (already covered by export * from "./definitions").
Conventions / gotchas
- Capacitor, not elizaOS Plugin. Import
Contactsfrom this package and call its methods; do not try to load it viaelizaOS's plugin loader. - Instrumented test (issue #9967). The
ContactsContractquery lives inContactsReaderand is covered by an on-device write→read round-trip (android/src/androidTest/.../ContactsReaderInstrumentedTest.kt,GrantPermissionRule): insert a contact → read it back → assert name+phone → clean up. Run via./gradlew :elizaos-capacitor-contacts:connectedDebugAndroidTestfrompackages/app-core/platforms/android.listContactsandcreateContact's summary both delegate to the reader (JS shape unchanged). - Android only for writes.
createContactandimportVCardare hard-fails on web. Design any elizaOS action that calls them to check the platform first. - Permissions are feature-gated, not app-required. The plugin declares the
contactsalias (READ_CONTACTS/WRITE_CONTACTS) in@CapacitorPlugin(permissions=…), so the Capacitor basePluginauto-providescheckPermissions()/requestPermissions()({ contacts: PermissionState }; web returnsgranted). The Contacts view callsrequestPermissions()on first open (idempotent — already-granted never re-prompts) and shows a grant-in-settings message if denied. Nothing requests contacts at app launch. The bridge methods still reject if not granted (defensive); do NOT add a launch-time or app-wide contacts gate. - limit guard.
listContactsenforces1 ≤ limit ≤ 500; requests outside that range are rejected. - vCard parser is internal.
parseVCardsinContactsPlugin.kthandles RFC 6350 line folding and theFN/N/TEL/EMAILproperties. It intentionally ignores other vCard fields. Photo data is not imported. - Build output. The published package ships
dist/esm/(ESM, consumed by bundlers) anddist/plugin.cjs.js(CJS). Thebun/developmentexport condition points directly tosrc/index.tsfor zero-build dev. - Peer dep.
@capacitor/core ^8.3.1must be present in the consuming app.
⛔ NON-NEGOTIABLE — evidence, trajectories & real end-to-end tests
The binding, repo-wide standard is AGENTS.md. Read it. Nothing in this package is done until it is proven done — a reviewer must confirm it works without reading the code, from the artifacts you attach. This applies to every feature, fix, refactor, and chore here. "Tests pass" is not proof; "CI is green" is not proof.
- Record AND read model trajectories. Capture the actual inputs and outputs of the model
from a live LLM — not the deterministic proxy, not a mock: the prompt, the
providers/context, the raw model output, every tool/action call, and the result. Then open
the trajectory and review it by hand. A captured-but-unread trajectory is not evidence
(
packages/scenario-runner/bin/eliza-scenarios run <scenario> --report <out>). - Real, full-featured E2E — no larp. Every feature ships detailed end-to-end tests that drive the real path end to end. Not the happy "front door" only: cover error paths, edge/empty/invalid input, concurrency, roles/permissions, and adversarial input. A test that asserts against a mock/stub/fixture standing in for the thing under test does not count. If the real model/device/chain/connector/account is hard to reach, make it reachable — that is the work, not an excuse to mock. If the existing tests here are shallow or mocked, fixing them is part of your change.
- Screenshots + logs at every phase, plus a complete walkthrough video/run-through of
the entire feature or view, start to finish (
bun run test:e2e:record). - Manually review every artifact the change touches — never just the green check: client
logs (console + network), server logs (
[ClassName] …), the model trajectories in and out, before/after full-page screenshots, and the domain artifacts listed below for this package. - No residuals. No shortcuts. The goal is not "done" — it is everything done. Clear every blocker by the hard path: build the real architecture, stand up the real model/device/service, actually test it. Never leave a TODO, a stub, a stepping-stone, or a "follow-up." When unsure, research thoroughly, weigh the options, and ship the best, highest-effort, production-ready version. Keep going until every possibility is exhausted.
Artifacts → attached inline in the PR (MP4 video, JPG screenshots, logs in <details>); attach each evidence type or
explicitly mark it N/A with a reason — never leave it blank. If develop moved and changed
behavior, re-capture evidence; stale proof is worse than none.
Capture & manually review for this package — native / on-device bridge:
- The capability run on a real device or simulator — not desktop Chromium against a mocked bridge (see #9967/#9580): device logs + the captured output (photo, OCR text, detection boxes, transcript, sensor reading).
- Parity vs the reference implementation where one exists (e.g. the Python/Ultralytics reference), with the numeric tolerances actually met.
- Permission-denied, no-hardware, and background/foreground lifecycle paths.
- A short recording of the on-device run; confirm the build under test is yours (versionName / a known on-screen change), not a stale install.