From 28b1f96b507f34b0fab9df6d71dbf456cebae669 Mon Sep 17 00:00:00 2001 From: Danya H Date: Mon, 28 Sep 2026 01:56:09 +0100 Subject: [PATCH] feat(rpc): introduce agent skill for extended usage --- readme.md | 2 + rpc/package.json | 1 + rpc/readme.md | 10 ++++ rpc/skills/fivem-rpc/SKILL.md | 105 ++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+) create mode 100644 rpc/skills/fivem-rpc/SKILL.md diff --git a/readme.md b/readme.md index 5abcb51..1996a72 100644 --- a/readme.md +++ b/readme.md @@ -72,6 +72,8 @@ All methods: [API reference](rpc/readme.md). Issues and pull requests are very welcome +When the API changes, update the TSDoc, the [direction table](rpc/readme.md#directions) and the [agent skill](rpc/skills/fivem-rpc/SKILL.md) + Releases are published with `bun publish`, which replaces the `workspace:^` dependency on shared-types with its version (`npm publish` would not) ## License diff --git a/rpc/package.json b/rpc/package.json index 3b1a0c3..3a67501 100644 --- a/rpc/package.json +++ b/rpc/package.json @@ -32,6 +32,7 @@ }, "files": [ "dist/**/*", + "skills/**/*", "readme.md", "license.md" ], diff --git a/rpc/readme.md b/rpc/readme.md index 8176e12..8e4ee18 100644 --- a/rpc/readme.md +++ b/rpc/readme.md @@ -421,6 +421,16 @@ const response = await rpc.emitSelf('webviewEvent', someData) // response will come from webview listener with returned data ``` +## Using with AI agents + +The package ships an [Agent Skill](https://agentskills.io) with the directions, rules, typing and error fixes above. Copy it into your agent's skills folder, e.g. for Claude Code: + +```bash +cp -r node_modules/@entityseven/fivem-rpc/skills/fivem-rpc .claude/skills/ +``` + +Copy it again after upgrading. Every method also carries TSDoc with its direction and matching listener + ## License Licensed under the [Custom Attribution-NoDerivs Software License](license.md) diff --git a/rpc/skills/fivem-rpc/SKILL.md b/rpc/skills/fivem-rpc/SKILL.md new file mode 100644 index 0000000..77f0dc8 --- /dev/null +++ b/rpc/skills/fivem-rpc/SKILL.md @@ -0,0 +1,105 @@ +--- +name: fivem-rpc +description: Use when writing FiveM server, client or NUI (webview) code that calls between environments with @entityseven/fivem-rpc (createRPC, emitServer, onClient, emitWebview, onServer, ...), or when declaring its typed events in @entityseven/fivem-rpc-shared-types. +--- + +# @entityseven/fivem-rpc + +Typed async calls between FiveM server, client and webview (NUI). Every `emit*` resolves with the return value of the matching `on*` listener in another environment + +## Setup + +One instance per environment, created once in a local module and imported everywhere else: + +```ts +// server/rpc.ts, same in client/rpc.ts with env 'client' and webview/rpc.ts with env 'webview' +import { createRPC } from '@entityseven/fivem-rpc' +export const rpc = createRPC({ env: 'server' }) // options: debug (false), timeout (5000 ms, 0 = none) +``` + +## Directions + +Method names are relative to the environment: `onClient` on the server listens to clients, `onClient` in the webview listens to its client + +| From | Call | To | Listener | Typed by | +| ------- | ------------------------------------- | ----------- | ------------------------------------- | ------------------------- | +| server | `emitClient(player, event, ...args)` | client | `onServer` | `RPCEvents_ServerClient` | +| server | `emitClientEveryone(event, ...args)` | all clients | `onServer`, no response | `RPCEvents_ServerClient` | +| server | `emitWebview(player, event, ...args)` | webview | `onServer`, via client | `RPCEvents_ServerWebview` | +| server | `emitSelf(event, ...args)` | server | `onSelf` | `RPCEvents_Server` | +| client | `emitServer(event, ...args)` | server | `onClient`, player first | `RPCEvents_ClientServer` | +| client | `emitWebview(event, ...args)` | webview | `onClient` | `RPCEvents_ClientWebview` | +| client | `emitSelf(event, ...args)` | client | `onSelf` | `RPCEvents_Client` | +| webview | `emitServer(event, ...args)` | server | `onWebview`, player first, via client | `RPCEvents_WebviewServer` | +| webview | `emitClient(event, ...args)` | client | `onWebview` | `RPCEvents_WebviewClient` | +| webview | `emitSelf(event, ...args)` | webview | `onSelf` | `RPCEvents_Webview` | + +Commands registered with `onCommand` are typed by `RPCCommands_Server` and `RPCCommands_Client` + +## Rules + +- every `emit*` needs its listener registered in the target environment (see Directions), otherwise it rejects with `EVENT_NOT_REGISTERED` +- webview <-> server always goes through the player's client: the client must call `createRPC({ env: 'client' })` even with no listeners, otherwise those calls time out +- one listener per event and direction: registering the same name again replaces it, `off*` removes it +- always `await` or `.catch()` an `emit*`: it rejects with `RPCError`. `emitClientEveryone` is one-way and resolves once sent +- server `onClient` and `onWebview` listeners get the caller's server id first, taken from FiveM `source`. Use it, never trust player ids passed as arguments +- arguments and return values travel as JSON: pass plain data (no functions, class instances, `Map`, `Set`) +- never register FiveM events or NUI callbacks named `__rpc:*`, the library owns them +- `onNativeEvent` and `onNativeNetworkEvent` only accept names from `NATIVE_SERVER_EVENTS`, `NATIVE_CLIENT_EVENTS` and `NATIVE_CLIENT_NETWORK_EVENTS`. Use FiveM `on(name, cb)` for anything else + +## Typing + +Declare events in one `.d.ts` file included by the `tsconfig.json` of every environment. Without declarations every name, argument and result is `any` + +```ts +// shared/rpc.d.ts +import '@entityseven/fivem-rpc-shared-types' // required: without it the declaration replaces the module + +declare module '@entityseven/fivem-rpc-shared-types' { + interface RPCEvents_WebviewServer { + // event name(arguments): value the listener returns + buyItem(item: string): boolean + } + interface RPCEvents_ServerClient { + itemBought(item: string): void + } + interface RPCCommands_Server { + ban: true // commands are keys, the value is not used + } +} +``` + +Leave an interface empty to keep that direction untyped. Declare plain return values, listeners may still be `async` + +## Example + +```ts +// server +rpc.onWebview('buyItem', async (player, item) => { + const ok = await chargePlayer(player, item) + if (ok) await rpc.emitClient(player, 'itemBought', item) + return ok +}) + +// client +rpc.onServer('itemBought', item => { + // update HUD, play a sound +}) + +// webview +const ok = await rpc.emitServer('buyItem', 'water') +``` + +## Errors + +`RPCError.code` is one of `RPCErrors`, the message names the fix + +| Code | Cause | Fix | +| ---------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `EVENT_NOT_REGISTERED` | no listener for the event on the target | register the listener from the Directions table on the target | +| `TIMEOUT` | no response within `timeout` (default 5000 ms) | listener must return or resolve; client needs `createRPC` for webview <-> server; raise `timeout` | +| `HANDLER_ERROR` | the listener threw, its message is included | fix the listener, the stack is logged with `console.error` on the target | +| `UNKNOWN_NATIVE` | `onNative*` name not in its `NATIVE_*` list | use FiveM `on(name, cb)` | +| `UNKNOWN_ENVIRONMENT` | `createRPC` got an `env` other than server, client, webview | fix `env` | + +Debug with `createRPC({ env, debug: true })`: logs every registration, call and payload