@calimero-network/agent-skills 0.2.0 → 0.4.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.
Files changed (78) hide show
  1. package/README.md +137 -17
  2. package/SKILL.md +31 -23
  3. package/package.json +2 -2
  4. package/scripts/install.js +3 -3
  5. package/scripts/test.js +6 -15
  6. package/skills/calimero-abi-codegen/SKILL.md +121 -22
  7. package/skills/calimero-abi-codegen/references/abi-format.md +3 -5
  8. package/skills/calimero-abi-codegen/references/generated-output.md +12 -4
  9. package/skills/calimero-abi-codegen/rules/schema-version.md +11 -4
  10. package/skills/calimero-abi-codegen/rules/unique-names.md +2 -6
  11. package/skills/calimero-client-js/SKILL.md +127 -22
  12. package/skills/calimero-client-js/references/auth.md +18 -10
  13. package/skills/calimero-client-js/references/rpc-calls.md +15 -21
  14. package/skills/calimero-client-js/references/sso.md +9 -9
  15. package/skills/calimero-client-js/references/websocket-events.md +73 -59
  16. package/skills/calimero-client-js/rules/camelcase-api.md +10 -7
  17. package/skills/calimero-client-js/rules/token-refresh.md +59 -21
  18. package/skills/calimero-client-py/SKILL.md +26 -10
  19. package/skills/calimero-client-py/references/api.md +41 -43
  20. package/skills/calimero-client-py/references/auth.md +7 -7
  21. package/skills/calimero-client-py/rules/async-usage.md +27 -31
  22. package/skills/calimero-client-py/rules/stable-node-name.md +7 -7
  23. package/skills/calimero-core/SKILL.md +135 -0
  24. package/skills/calimero-core/references/architecture.md +101 -0
  25. package/skills/calimero-core/references/jsonrpc-protocol.md +192 -0
  26. package/skills/calimero-core/references/namespaces-groups.md +94 -0
  27. package/skills/calimero-core/references/storage-types.md +118 -0
  28. package/skills/calimero-core/references/websocket-events.md +142 -0
  29. package/skills/calimero-core/rules/context-is-not-app.md +35 -0
  30. package/skills/calimero-core/rules/crdt-types-only.md +55 -0
  31. package/skills/calimero-desktop/SKILL.md +25 -14
  32. package/skills/calimero-desktop/references/sso-integration.md +49 -22
  33. package/skills/calimero-desktop/rules/sso-fallback.md +3 -2
  34. package/skills/calimero-merobox/SKILL.md +255 -28
  35. package/skills/calimero-merobox/references/ci-integration.md +3 -2
  36. package/skills/calimero-merobox/references/workflow-files.md +7 -5
  37. package/skills/calimero-merobox/rules/docker-required.md +7 -6
  38. package/skills/calimero-meroctl/SKILL.md +68 -0
  39. package/skills/calimero-meroctl/references/commands.md +177 -0
  40. package/skills/calimero-meroctl/references/scripting.md +80 -0
  41. package/skills/calimero-meroctl/rules/call-view-flag.md +28 -0
  42. package/skills/calimero-meroctl/rules/register-node-once.md +34 -0
  43. package/skills/calimero-merod/SKILL.md +49 -0
  44. package/skills/calimero-merod/references/health-endpoints.md +90 -0
  45. package/skills/calimero-merod/references/init-flags.md +84 -0
  46. package/skills/calimero-merod/rules/init-before-run.md +40 -0
  47. package/skills/calimero-merod/rules/port-assignments.md +33 -0
  48. package/skills/calimero-node/SKILL.md +52 -35
  49. package/skills/calimero-node/references/context-lifecycle.md +34 -17
  50. package/skills/calimero-node/references/meroctl-commands.md +89 -99
  51. package/skills/calimero-node/rules/app-vs-context.md +4 -4
  52. package/skills/calimero-registry/SKILL.md +110 -31
  53. package/skills/calimero-registry/references/bundle-and-push.md +99 -34
  54. package/skills/calimero-registry/references/manifest-format.md +56 -35
  55. package/skills/calimero-registry/references/mero-sign.md +10 -9
  56. package/skills/calimero-registry/rules/key-security.md +3 -2
  57. package/skills/calimero-registry/rules/sign-before-pack.md +5 -5
  58. package/skills/calimero-rust-sdk/SKILL.md +154 -44
  59. package/skills/calimero-rust-sdk/references/blob-api.md +119 -0
  60. package/skills/calimero-rust-sdk/references/event-handlers.md +122 -0
  61. package/skills/calimero-rust-sdk/references/events.md +2 -1
  62. package/skills/calimero-rust-sdk/references/examples.md +81 -29
  63. package/skills/calimero-rust-sdk/references/migrations.md +123 -0
  64. package/skills/calimero-rust-sdk/references/nested-crdts.md +113 -0
  65. package/skills/calimero-rust-sdk/references/private-storage.md +76 -34
  66. package/skills/calimero-rust-sdk/references/state-collections.md +106 -21
  67. package/skills/calimero-rust-sdk/references/user-and-frozen-storage.md +169 -0
  68. package/skills/calimero-rust-sdk/rules/app-macro-placement.md +5 -2
  69. package/skills/calimero-rust-sdk/rules/no-std-collections.md +5 -2
  70. package/skills/calimero-rust-sdk/rules/state-derives.md +45 -0
  71. package/skills/calimero-rust-sdk/rules/wasm-constraints.md +12 -10
  72. package/skills/calimero-sdk-js/SKILL.md +145 -0
  73. package/skills/calimero-sdk-js/references/build-pipeline.md +98 -0
  74. package/skills/calimero-sdk-js/references/collections.md +132 -0
  75. package/skills/calimero-sdk-js/references/events.md +63 -0
  76. package/skills/calimero-sdk-js/rules/crdt-only-state.md +47 -0
  77. package/skills/calimero-sdk-js/rules/no-console-log.md +38 -0
  78. package/skills/calimero-sdk-js/rules/view-decorator.md +48 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  Running `calimero-abi-codegen -i abi.json -o src/generated --client-name KvStoreClient` produces:
4
4
 
5
- ```
5
+ ```text
6
6
  src/generated/
7
7
  ├── types.ts
8
8
  └── KvStoreClient.ts
@@ -36,7 +36,10 @@ import { CalimeroApp, Context } from '@calimero-network/calimero-client';
36
36
  import type { Entry } from './types';
37
37
 
38
38
  export class KvStoreClient {
39
- constructor(private app: CalimeroApp, private context: Context) {}
39
+ constructor(
40
+ private app: CalimeroApp,
41
+ private context: Context
42
+ ) {}
40
43
 
41
44
  async set(key: string, value: string): Promise<void> {
42
45
  await this.app.mutate(this.context, 'set', { key, value });
@@ -55,7 +58,12 @@ export class KvStoreClient {
55
58
  ## Using the generated client
56
59
 
57
60
  ```typescript
58
- import { CalimeroApp, Context, getJWTObject, getStorageAppEndpointKey } from '@calimero-network/calimero-client';
61
+ import {
62
+ CalimeroApp,
63
+ Context,
64
+ getJWTObject,
65
+ getStorageAppEndpointKey,
66
+ } from '@calimero-network/calimero-client';
59
67
  import { KvStoreClient } from './generated/KvStoreClient';
60
68
 
61
69
  const app = new CalimeroApp(getStorageAppEndpointKey(), getJWTObject()?.access_token);
@@ -65,7 +73,7 @@ const client = new KvStoreClient(app, context);
65
73
  // Fully typed — IDE autocomplete works
66
74
  await client.set('hello', 'world');
67
75
  const value = await client.get('hello'); // string | null
68
- const all = await client.entries(); // Entry[]
76
+ const all = await client.entries(); // Entry[]
69
77
  ```
70
78
 
71
79
  ## Regenerating after app changes
@@ -1,19 +1,24 @@
1
1
  # Rule: ABI manifest must have schema_version "wasm-abi/1"
2
2
 
3
- The codegen tool rejects any ABI file that doesn't have the exact string `"wasm-abi/1"` as its `schema_version`.
3
+ The codegen tool rejects any ABI file that doesn't have the exact string `"wasm-abi/1"` as its
4
+ `schema_version`.
4
5
 
5
6
  ## WRONG:
6
7
 
7
8
  ```json
8
9
  {
9
10
  "schema_version": "1",
10
- "types": {}, "methods": [], "events": []
11
+ "types": {},
12
+ "methods": [],
13
+ "events": []
11
14
  }
12
15
  ```
13
16
 
14
17
  ```json
15
18
  {
16
- "types": {}, "methods": [], "events": []
19
+ "types": {},
20
+ "methods": [],
21
+ "events": []
17
22
  }
18
23
  ```
19
24
 
@@ -22,7 +27,9 @@ The codegen tool rejects any ABI file that doesn't have the exact string `"wasm-
22
27
  ```json
23
28
  {
24
29
  "schema_version": "wasm-abi/1",
25
- "types": {}, "methods": [], "events": []
30
+ "types": {},
31
+ "methods": [],
32
+ "events": []
26
33
  }
27
34
  ```
28
35
 
@@ -20,9 +20,7 @@ The ABI validator rejects manifests where names clash — across methods, events
20
20
  "types": {
21
21
  "set": { "kind": "record", "fields": [] }
22
22
  },
23
- "methods": [
24
- { "name": "set", "params": [] }
25
- ]
23
+ "methods": [{ "name": "set", "params": [] }]
26
24
  }
27
25
  ```
28
26
 
@@ -43,9 +41,7 @@ Map keys **must** be `{ "kind": "string" }`.
43
41
  ```json
44
42
  {
45
43
  "types": {},
46
- "methods": [
47
- { "name": "get", "returns": { "$ref": "NonExistentType" } }
48
- ]
44
+ "methods": [{ "name": "get", "returns": { "$ref": "NonExistentType" } }]
49
45
  }
50
46
  ```
51
47
 
@@ -1,44 +1,135 @@
1
1
  # calimero-client-js — Agent Instructions
2
2
 
3
- You are helping a developer connect a **browser or Node.js frontend** to a Calimero node using `@calimero-network/calimero-client` or `@calimero-network/mero-js`.
3
+ You are helping a developer connect a **browser or Node.js frontend** to a Calimero node using
4
+ `@calimero-network/mero-react` (preferred) or `@calimero-network/calimero-client`.
5
+
6
+ > **NOT this skill** if the developer is building the application logic that _runs on the node_ in
7
+ > TypeScript — that is `calimero-sdk-js`. This skill is for the _client_ side: auth, RPC calls, and
8
+ > WebSocket subscriptions from a browser or React app.
4
9
 
5
10
  ## Package versions
6
11
 
7
- | Package | Version | Notes |
8
- | --- | --- | --- |
9
- | `@calimero-network/calimero-client` | latest | Stable client for browser/Node auth, RPC, WebSocket |
10
- | `@calimero-network/mero-js` | `>=2.0.0-beta.1` | v2 API all request fields are **camelCase** |
12
+ | Package | Notes |
13
+ | ----------------------------------- | ----------------------------------------------------------------------------------------- |
14
+ | `@calimero-network/mero-react` | **Preferred for React apps.** Exports `MeroJs`, `useSubscription`, `MeroProvider`, hooks. |
15
+ | `@calimero-network/mero-js` | Core SDK. Zero deps. Used standalone in non-React contexts. |
16
+ | `@calimero-network/calimero-client` | Legacy client. Still works; new projects should prefer mero-react/mero-js. |
11
17
 
12
18
  ## Critical: mero-js v2 uses camelCase
13
19
 
14
- v2 changed all request field names from `snake_case` to `camelCase`.
20
+ All request field names changed from `snake_case` to `camelCase` in v2.
15
21
 
16
22
  ```typescript
17
- // WRONG (v1 / old):
23
+ // WRONG (v1):
18
24
  { context_id: '...', context_identity: '...' }
19
25
 
20
- // CORRECT (v2):
26
+ // CORRECT (v2 / mero-react):
21
27
  { contextId: '...', contextIdentity: '...' }
22
28
  ```
23
29
 
24
- This applies to `GenerateClientKeyRequest` and all other request types.
30
+ ## React app pattern (mero-react) recommended
25
31
 
26
- ## Install
32
+ ### Install
27
33
 
28
34
  ```bash
29
- pnpm add @calimero-network/calimero-client
30
- # or
31
- pnpm add @calimero-network/mero-js
35
+ pnpm add @calimero-network/mero-react
32
36
  ```
33
37
 
34
- ## Core workflow
38
+ ### Setup provider
39
+
40
+ ```tsx
41
+ import { MeroProvider } from '@calimero-network/mero-react';
42
+
43
+ function App() {
44
+ return (
45
+ <MeroProvider nodeUrl="http://localhost:2428">
46
+ <YourApp />
47
+ </MeroProvider>
48
+ );
49
+ }
50
+ ```
35
51
 
36
- 1. On startup: read SSO tokens from URL hash (if opened by Desktop), otherwise check `localStorage` for existing session, otherwise show login
37
- 2. Store tokens using the provided storage helpers (`setAppEndpointKey`, `setAccessToken`, etc.)
38
- 3. Call app methods via the `rpcClient` singleton using `rpcClient.execute()`
39
- 4. Subscribe to events via `WsSubscriptionsClient`
52
+ ### Call app methods
40
53
 
41
- ## Minimal working example
54
+ Generated clients (from abi-codegen) import `MeroJs` from `@calimero-network/mero-react`:
55
+
56
+ ```typescript
57
+ import { MeroJs } from '@calimero-network/mero-react';
58
+
59
+ export class KvClient {
60
+ constructor(
61
+ private mero: MeroJs,
62
+ private contextId: string,
63
+ private executorPublicKey: string
64
+ ) {}
65
+
66
+ async set(key: string, value: string): Promise<void> {
67
+ await this.mero.rpc.execute({
68
+ contextId: this.contextId,
69
+ method: 'set',
70
+ argsJson: { key, value },
71
+ executorPublicKey: this.executorPublicKey,
72
+ });
73
+ }
74
+
75
+ async get(key: string): Promise<string | null> {
76
+ const response = await this.mero.rpc.execute({
77
+ contextId: this.contextId,
78
+ method: 'get',
79
+ argsJson: { key },
80
+ executorPublicKey: this.executorPublicKey,
81
+ });
82
+ return response as string | null;
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### Subscribe to events (mero-react hook)
88
+
89
+ ```typescript
90
+ import { useSubscription } from '@calimero-network/mero-react';
91
+
92
+ // Subscribe to one or more contexts
93
+ useSubscription([contextId], (event: { contextId: string; data: unknown }) => {
94
+ // event.data may be:
95
+ // - { type: 'EventName', ...payload } for direct events
96
+ // - { events: [{ kind: string, data: unknown }] } for execution event batches
97
+ console.log('event:', event.data);
98
+ });
99
+
100
+ // Subscribe to multiple contexts (e.g. game + lobby simultaneously)
101
+ useSubscription([gameContextId, lobbyContextId], (event) => {
102
+ console.log('from context:', event.contextId);
103
+ });
104
+ ```
105
+
106
+ ### Parsing execution event payloads
107
+
108
+ Events from `app::emit!()` arrive batched in an `events` array. Each entry has `kind` (the variant
109
+ name) and `data` (the payload, possibly as a byte array):
110
+
111
+ ```typescript
112
+ function decodeEventData(data: unknown): unknown {
113
+ // If data is a number array, it's JSON-encoded bytes
114
+ if (Array.isArray(data) && data.every((n) => typeof n === 'number')) {
115
+ return JSON.parse(new TextDecoder().decode(new Uint8Array(data)));
116
+ }
117
+ return data;
118
+ }
119
+
120
+ // In the subscription callback:
121
+ useSubscription([contextId], (event) => {
122
+ const payload = event.data as any;
123
+ if (Array.isArray(payload?.events)) {
124
+ for (const e of payload.events) {
125
+ const decoded = decodeEventData(e.data);
126
+ console.log(e.kind, decoded); // e.g. "Inserted", { key: "...", value: "..." }
127
+ }
128
+ }
129
+ });
130
+ ```
131
+
132
+ ## Legacy calimero-client pattern
42
133
 
43
134
  ```typescript
44
135
  import {
@@ -68,7 +159,7 @@ const response = await rpcClient.execute<{ key: string }, string | null>({
68
159
  });
69
160
  console.log(response.result?.output);
70
161
 
71
- // 3. Subscribe to real-time events
162
+ // 3. Subscribe to events
72
163
  const ws = new WsSubscriptionsClient(getAppEndpointKey()!, '/ws');
73
164
  await ws.connect();
74
165
  ws.subscribe([getContextId()!]);
@@ -81,7 +172,21 @@ ws.addCallback((event) => {
81
172
  });
82
173
  ```
83
174
 
175
+ ## Core workflow
176
+
177
+ 1. On startup: read SSO tokens from URL hash (if opened by Desktop), otherwise check `localStorage`,
178
+ otherwise show login
179
+ 2. Store tokens using storage helpers or the `MeroProvider` (handles this automatically)
180
+ 3. Call app methods via the generated typed client or `mero.rpc.execute()`
181
+ 4. Subscribe to events via `useSubscription` (React) or `WsSubscriptionsClient`
182
+
183
+ ## Related skills
184
+
185
+ - **`calimero-core`** — JSON-RPC protocol spec, WebSocket event schemas, context/app/identity model
186
+ - **`calimero-desktop`** — SSO token passing from Calimero Desktop (URL hash flow)
187
+ - **`calimero-node`** — node setup if the developer is also running the node
188
+
84
189
  ## References
85
190
 
86
- See `references/` for auth flow, RPC calls, WebSocket events, and SSO.
87
- See `rules/` for camelCase API and token refresh requirements.
191
+ See `references/` for auth flow, RPC calls, WebSocket events, and SSO. See `rules/` for camelCase
192
+ API and token refresh requirements.
@@ -19,7 +19,7 @@ import {
19
19
  getContextId,
20
20
  setExecutorPublicKey,
21
21
  getExecutorPublicKey,
22
- setContextAndIdentityFromJWT, // extracts + stores contextId + executorPublicKey from JWT
22
+ setContextAndIdentityFromJWT, // extracts + stores contextId + executorPublicKey from JWT
23
23
  // App ID
24
24
  setApplicationId,
25
25
  getApplicationId,
@@ -27,9 +27,9 @@ import {
27
27
  setAuthEndpointURL,
28
28
  getAuthEndpointURL,
29
29
  // Decoded JWT payload
30
- getJWTObject, // returns { context_id, context_identity, exp, permissions, ... }
30
+ getJWTObject, // returns { context_id, context_identity, exp, permissions, ... }
31
31
  // Full auth config (throws if required fields are missing)
32
- getAuthConfig, // returns { appEndpointKey, contextId, executorPublicKey, jwtToken }
32
+ getAuthConfig, // returns { appEndpointKey, contextId, executorPublicKey, jwtToken }
33
33
  // Logout (clears all tokens + reloads)
34
34
  clientLogout,
35
35
  } from '@calimero-network/calimero-client';
@@ -44,10 +44,10 @@ When the app is opened by Desktop, tokens arrive in the URL hash. Store them:
44
44
  ```typescript
45
45
  function initFromDesktopSSO(): boolean {
46
46
  const hash = new URLSearchParams(window.location.hash.slice(1));
47
- const accessToken = hash.get('access_token');
47
+ const accessToken = hash.get('access_token');
48
48
  const refreshToken = hash.get('refresh_token');
49
- const nodeUrl = hash.get('node_url');
50
- const appId = hash.get('application_id');
49
+ const nodeUrl = hash.get('node_url');
50
+ const appId = hash.get('application_id');
51
51
 
52
52
  if (!accessToken || !nodeUrl) return false;
53
53
 
@@ -72,7 +72,12 @@ function initFromDesktopSSO(): boolean {
72
72
  ## Manual login (no Desktop)
73
73
 
74
74
  ```typescript
75
- import { setAppEndpointKey, setAccessToken, setRefreshToken, setContextAndIdentityFromJWT } from '@calimero-network/calimero-client';
75
+ import {
76
+ setAppEndpointKey,
77
+ setAccessToken,
78
+ setRefreshToken,
79
+ setContextAndIdentityFromJWT,
80
+ } from '@calimero-network/calimero-client';
76
81
 
77
82
  async function login(nodeUrl: string, accessToken: string, refreshToken: string) {
78
83
  setAppEndpointKey(nodeUrl);
@@ -126,8 +131,11 @@ clientLogout();
126
131
 
127
132
  ```typescript
128
133
  import {
129
- setAppEndpointKey, setAccessToken, setRefreshToken,
130
- setApplicationId, setContextAndIdentityFromJWT,
134
+ setAppEndpointKey,
135
+ setAccessToken,
136
+ setRefreshToken,
137
+ setApplicationId,
138
+ setContextAndIdentityFromJWT,
131
139
  getAuthConfig,
132
140
  } from '@calimero-network/calimero-client';
133
141
 
@@ -135,7 +143,7 @@ async function bootstrap() {
135
143
  // Try SSO from Desktop hash
136
144
  const hash = new URLSearchParams(window.location.hash.slice(1));
137
145
  const accessToken = hash.get('access_token');
138
- const nodeUrl = hash.get('node_url');
146
+ const nodeUrl = hash.get('node_url');
139
147
 
140
148
  if (accessToken && nodeUrl) {
141
149
  setAppEndpointKey(nodeUrl);
@@ -4,15 +4,11 @@ Calling application methods on a Calimero node.
4
4
 
5
5
  ## The `rpcClient` singleton
6
6
 
7
- `@calimero-network/calimero-client` exports a pre-configured `rpcClient` singleton.
8
- Import it directly — do not construct `JsonRpcClient` manually.
7
+ `@calimero-network/calimero-client` exports a pre-configured `rpcClient` singleton. Import it
8
+ directly — do not construct `JsonRpcClient` manually.
9
9
 
10
10
  ```typescript
11
- import {
12
- rpcClient,
13
- getContextId,
14
- getExecutorPublicKey,
15
- } from '@calimero-network/calimero-client';
11
+ import { rpcClient, getContextId, getExecutorPublicKey } from '@calimero-network/calimero-client';
16
12
  ```
17
13
 
18
14
  ---
@@ -23,7 +19,9 @@ import {
23
19
  const response = await rpcClient.execute<ArgsType, OutputType>({
24
20
  contextId: getContextId()!,
25
21
  method: 'methodName',
26
- argsJson: { /* your args */ },
22
+ argsJson: {
23
+ /* your args */
24
+ },
27
25
  executorPublicKey: getExecutorPublicKey()!,
28
26
  });
29
27
 
@@ -96,20 +94,20 @@ if (!response.error) {
96
94
 
97
95
  ## Error names
98
96
 
99
- | name | meaning |
100
- |---|---|
101
- | `FunctionCallError` | App method returned an error |
102
- | `RpcExecutionError` | Node couldn't execute the method |
97
+ | name | meaning |
98
+ | --------------------- | -------------------------------------------------- |
99
+ | `FunctionCallError` | App method returned an error |
100
+ | `RpcExecutionError` | Node couldn't execute the method |
103
101
  | `InvalidRequestError` | Malformed request (wrong context-id, missing args) |
104
- | `AuthenticationError` | JWT expired, revoked, or missing |
105
- | `UnknownServerError` | Unexpected server error |
102
+ | `AuthenticationError` | JWT expired, revoked, or missing |
103
+ | `UnknownServerError` | Unexpected server error |
106
104
 
107
105
  ---
108
106
 
109
107
  ## Error handling with 401
110
108
 
111
- The HTTP client inside `rpcClient` automatically refreshes tokens on `401 token_expired`.
112
- For `token_revoked` or `invalid_token` you need to re-authenticate:
109
+ The HTTP client inside `rpcClient` automatically refreshes tokens on `401 token_expired`. For
110
+ `token_revoked` or `invalid_token` you need to re-authenticate:
113
111
 
114
112
  ```typescript
115
113
  const response = await rpcClient.execute({ ... });
@@ -133,11 +131,7 @@ if (response.error) {
133
131
  ## Complete example: typed wrapper
134
132
 
135
133
  ```typescript
136
- import {
137
- rpcClient,
138
- getContextId,
139
- getExecutorPublicKey,
140
- } from '@calimero-network/calimero-client';
134
+ import { rpcClient, getContextId, getExecutorPublicKey } from '@calimero-network/calimero-client';
141
135
 
142
136
  async function setItem(key: string, value: string): Promise<void> {
143
137
  const response = await rpcClient.execute<{ key: string; value: string }, void>({
@@ -1,15 +1,15 @@
1
1
  # SSO — Calimero Desktop Integration
2
2
 
3
- When a user opens your app from Calimero Desktop, auth tokens are passed via the URL hash.
4
- Reading them lets users skip the manual login flow entirely.
3
+ When a user opens your app from Calimero Desktop, auth tokens are passed via the URL hash. Reading
4
+ them lets users skip the manual login flow entirely.
5
5
 
6
6
  ## Hash parameters
7
7
 
8
- | Parameter | Description |
9
- | --- | --- |
10
- | `access_token` | JWT for authenticated node calls |
11
- | `refresh_token` | Token to obtain a new access token |
12
- | `node_url` | URL of the local node to connect to |
8
+ | Parameter | Description |
9
+ | ---------------- | ------------------------------------------ |
10
+ | `access_token` | JWT for authenticated node calls |
11
+ | `refresh_token` | Token to obtain a new access token |
12
+ | `node_url` | URL of the local node to connect to |
13
13
  | `application_id` | The installed application's ID on the node |
14
14
 
15
15
  ## Reading from hash
@@ -68,5 +68,5 @@ async function initApp() {
68
68
 
69
69
  ## Important
70
70
 
71
- Always fall back to manual login if the hash params are absent — the app must work
72
- when opened directly in a browser, not only from Desktop.
71
+ Always fall back to manual login if the hash params are absent — the app must work when opened
72
+ directly in a browser, not only from Desktop.
@@ -6,111 +6,117 @@ Subscribe to real-time events emitted by the application running on the node.
6
6
 
7
7
  The node sends two kinds of events to subscribers:
8
8
 
9
- | type | when | payload |
10
- |---|---|---|
11
- | `StateMutation` | Another member mutated shared state | `{ newRoot: string }` |
12
- | `ExecutionEvent` | App emitted `app::emit!()` | `{ events: ExecutionEvent[] }` |
9
+ | type | when | payload |
10
+ | ---------------- | ----------------------------------- | ------------------------------ |
11
+ | `StateMutation` | Another member mutated shared state | `{ newRoot: string }` |
12
+ | `ExecutionEvent` | App emitted `app::emit!()` | `{ events: ExecutionEvent[] }` |
13
13
 
14
- An `ExecutionEvent` has: `{ kind: string; data: any }` where `kind` matches the Rust event variant name.
14
+ An `ExecutionEvent` entry: `{ kind: string; data: any }` where `kind` matches the Rust event variant
15
+ name and `data` may be a byte array (UTF-8 JSON) or a plain object.
15
16
 
16
17
  ---
17
18
 
18
- ## Connect and subscribe
19
+ ## mero-react: useSubscription hook (preferred for React)
19
20
 
20
21
  ```typescript
21
- import {
22
- WsSubscriptionsClient,
23
- getAppEndpointKey,
24
- getContextId,
25
- } from '@calimero-network/calimero-client';
22
+ import { useSubscription } from '@calimero-network/mero-react';
23
+
24
+ function MyComponent({ contextId }: { contextId: string }) {
25
+ useSubscription([contextId], (event: { contextId: string; data: unknown }) => {
26
+ const payload = event.data as any;
27
+
28
+ // ExecutionEvent: data has an `events` array
29
+ if (Array.isArray(payload?.events)) {
30
+ for (const e of payload.events) {
31
+ const data = decodeEventData(e.data);
32
+ switch (e.kind) {
33
+ case 'Inserted':
34
+ console.log('inserted', data);
35
+ break;
36
+ case 'Removed':
37
+ console.log('removed', data);
38
+ break;
39
+ }
40
+ }
41
+ }
42
+ });
43
+ }
44
+
45
+ // Byte-array payloads are JSON-encoded; decode them:
46
+ function decodeEventData(data: unknown): unknown {
47
+ if (Array.isArray(data) && data.every((n) => typeof n === 'number')) {
48
+ try {
49
+ return JSON.parse(new TextDecoder().decode(new Uint8Array(data)));
50
+ } catch {
51
+ return data;
52
+ }
53
+ }
54
+ return data;
55
+ }
56
+ ```
26
57
 
27
- const nodeUrl = getAppEndpointKey()!;
28
- const ws = new WsSubscriptionsClient(nodeUrl, '/ws');
58
+ ## Subscribe to multiple contexts
29
59
 
30
- // Connect first, then subscribe and add callback
31
- await ws.connect();
32
- ws.subscribe([getContextId()!]);
33
- ws.addCallback((event) => {
34
- console.log('Event received:', event);
60
+ ```typescript
61
+ // Subscribe to game context + lobby context simultaneously
62
+ useSubscription([gameContextId, lobbyContextId], (event) => {
63
+ console.log('event from context:', event.contextId);
35
64
  });
36
65
  ```
37
66
 
38
67
  ---
39
68
 
40
- ## Handling ExecutionEvents from app logic
69
+ ## Legacy calimero-client: WsSubscriptionsClient
41
70
 
42
71
  ```typescript
43
- import type {
44
- NodeEvent,
45
- ExecutionEventPayload,
46
- StateMutationPayload,
72
+ import {
73
+ WsSubscriptionsClient,
74
+ getAppEndpointKey,
75
+ getContextId,
47
76
  } from '@calimero-network/calimero-client';
48
77
 
49
- ws.addCallback((event: NodeEvent) => {
78
+ const ws = new WsSubscriptionsClient(getAppEndpointKey()!, '/ws');
79
+ await ws.connect();
80
+ ws.subscribe([getContextId()!]);
81
+ ws.addCallback((event) => {
50
82
  if (event.type === 'ExecutionEvent') {
51
83
  for (const e of event.data.events) {
52
- // e.kind matches the Rust event variant name, e.g. 'ItemAdded'
53
- // e.data contains the event payload
54
- switch (e.kind) {
55
- case 'ItemAdded':
56
- console.log('Item added:', e.data);
57
- addItemToUI(e.data.key, e.data.value);
58
- break;
59
- case 'ItemRemoved':
60
- removeItemFromUI(e.data.key);
61
- break;
62
- }
84
+ console.log(e.kind, e.data);
63
85
  }
64
86
  }
65
-
66
87
  if (event.type === 'StateMutation') {
67
- // Another member changed shared state — refresh data from node
68
- console.log('State root changed:', event.data.newRoot);
69
88
  refreshDataFromNode();
70
89
  }
71
90
  });
72
91
  ```
73
92
 
74
- ---
75
-
76
- ## Subscribe to multiple contexts
77
-
78
- ```typescript
79
- ws.subscribe([contextId1, contextId2]);
80
- // Events include event.contextId to identify which context emitted them
81
- ```
82
-
83
- ---
84
-
85
- ## Cleanup
93
+ Cleanup:
86
94
 
87
95
  ```typescript
88
- // Remove a specific callback
89
96
  ws.removeCallback(myCallback);
90
-
91
- // Unsubscribe from specific contexts
92
97
  ws.unsubscribe([contextId]);
93
-
94
- // Close connection entirely
95
98
  ws.disconnect();
96
99
  ```
97
100
 
98
101
  ---
99
102
 
100
- ## React hook pattern
103
+ ## React hook for legacy client
101
104
 
102
105
  ```typescript
103
106
  import { useEffect } from 'react';
104
- import { WsSubscriptionsClient, getAppEndpointKey, getContextId } from '@calimero-network/calimero-client';
107
+ import {
108
+ WsSubscriptionsClient,
109
+ getAppEndpointKey,
110
+ getContextId,
111
+ } from '@calimero-network/calimero-client';
105
112
 
106
- function useNodeEvents(onEvent: (event: NodeEvent) => void) {
113
+ function useNodeEvents(onEvent: (event: any) => void) {
107
114
  useEffect(() => {
108
115
  const nodeUrl = getAppEndpointKey();
109
116
  const contextId = getContextId();
110
117
  if (!nodeUrl || !contextId) return;
111
118
 
112
119
  const ws = new WsSubscriptionsClient(nodeUrl, '/ws');
113
-
114
120
  ws.connect().then(() => {
115
121
  ws.subscribe([contextId]);
116
122
  ws.addCallback(onEvent);
@@ -124,3 +130,11 @@ function useNodeEvents(onEvent: (event: NodeEvent) => void) {
124
130
  }, [onEvent]);
125
131
  }
126
132
  ```
133
+
134
+ ---
135
+
136
+ ## Connection notes
137
+
138
+ - `WsSubscriptionsClient` does **not** reconnect automatically — handle reconnect manually
139
+ - `useSubscription` from `mero-react` manages the connection lifecycle for you
140
+ - WebSocket tokens are **not** auto-refreshed unlike `rpcClient` HTTP calls