@calimero-network/agent-skills 0.1.1 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calimero-network/agent-skills",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "AI agent skills for Calimero Network development — Rust SDK, JS client, registry publishing, desktop SSO, and more.",
5
5
  "keywords": [
6
6
  "calimero",
@@ -33,10 +33,53 @@ pnpm add @calimero-network/mero-js
33
33
 
34
34
  ## Core workflow
35
35
 
36
- 1. Read auth tokens (from URL hash if opened by Desktop, otherwise prompt login)
37
- 2. Initialize client with `node_url` + tokens
38
- 3. Call app methods via JSON-RPC
39
- 4. Subscribe to events via WebSocket
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`
40
+
41
+ ## Minimal working example
42
+
43
+ ```typescript
44
+ import {
45
+ rpcClient,
46
+ getContextId,
47
+ getExecutorPublicKey,
48
+ getAppEndpointKey,
49
+ setAppEndpointKey,
50
+ setAccessToken,
51
+ setRefreshToken,
52
+ setContextAndIdentityFromJWT,
53
+ WsSubscriptionsClient,
54
+ } from '@calimero-network/calimero-client';
55
+
56
+ // 1. Store auth tokens (after SSO or login)
57
+ setAppEndpointKey('http://localhost:2428');
58
+ setAccessToken(accessToken);
59
+ setRefreshToken(refreshToken);
60
+ setContextAndIdentityFromJWT(accessToken); // extracts contextId + executorPublicKey
61
+
62
+ // 2. Call an app method
63
+ const response = await rpcClient.execute<{ key: string }, string | null>({
64
+ contextId: getContextId()!,
65
+ method: 'get',
66
+ argsJson: { key: 'hello' },
67
+ executorPublicKey: getExecutorPublicKey()!,
68
+ });
69
+ console.log(response.result?.output);
70
+
71
+ // 3. Subscribe to real-time events
72
+ const ws = new WsSubscriptionsClient(getAppEndpointKey()!, '/ws');
73
+ await ws.connect();
74
+ ws.subscribe([getContextId()!]);
75
+ ws.addCallback((event) => {
76
+ if (event.type === 'ExecutionEvent') {
77
+ for (const e of event.data.events) {
78
+ console.log(e.kind, e.data);
79
+ }
80
+ }
81
+ });
82
+ ```
40
83
 
41
84
  ## References
42
85
 
@@ -1,58 +1,163 @@
1
1
  # Authentication
2
2
 
3
- ## Login flow
3
+ ## Storage helpers
4
+
5
+ `@calimero-network/calimero-client` provides these storage functions (backed by `localStorage`):
4
6
 
5
7
  ```typescript
6
- import { setupWalletSelector } from '@near-wallet-selector/core';
7
- import { ClientLogin } from '@calimero-network/calimero-client';
8
-
9
- const client = new ClientLogin({ nodeUrl: 'http://localhost:2428' });
10
-
11
- // 1. Generate a client key for this device
12
- const { contextId, contextIdentity } = await client.generateClientKey({
13
- contextId: 'your-context-id',
14
- contextIdentity: 'your-identity',
15
- });
16
-
17
- // 2. Login — returns access_token + refresh_token
18
- const tokens = await client.login({
19
- contextId,
20
- contextIdentity,
21
- });
22
-
23
- // 3. Store tokens
24
- localStorage.setItem('access_token', tokens.access_token);
25
- localStorage.setItem('refresh_token', tokens.refresh_token);
8
+ import {
9
+ // Node URL
10
+ setAppEndpointKey,
11
+ getAppEndpointKey,
12
+ // JWT tokens
13
+ setAccessToken,
14
+ getAccessToken,
15
+ setRefreshToken,
16
+ getRefreshToken,
17
+ // Context and identity (extracted from JWT)
18
+ setContextId,
19
+ getContextId,
20
+ setExecutorPublicKey,
21
+ getExecutorPublicKey,
22
+ setContextAndIdentityFromJWT, // extracts + stores contextId + executorPublicKey from JWT
23
+ // App ID
24
+ setApplicationId,
25
+ getApplicationId,
26
+ // Auth endpoint (separate from node URL)
27
+ setAuthEndpointURL,
28
+ getAuthEndpointURL,
29
+ // Decoded JWT payload
30
+ getJWTObject, // returns { context_id, context_identity, exp, permissions, ... }
31
+ // Full auth config (throws if required fields are missing)
32
+ getAuthConfig, // returns { appEndpointKey, contextId, executorPublicKey, jwtToken }
33
+ // Logout (clears all tokens + reloads)
34
+ clientLogout,
35
+ } from '@calimero-network/calimero-client';
26
36
  ```
27
37
 
28
- ## Token storage
38
+ ---
39
+
40
+ ## SSO login (from Calimero Desktop — recommended path)
29
41
 
30
- Tokens should be stored in `localStorage` or `sessionStorage`. The client library reads
31
- them automatically if you use the provided storage helpers:
42
+ When the app is opened by Desktop, tokens arrive in the URL hash. Store them:
32
43
 
33
44
  ```typescript
34
- import { getStorageAppEndpointKey, getJWTObject } from '@calimero-network/calimero-client';
45
+ function initFromDesktopSSO(): boolean {
46
+ const hash = new URLSearchParams(window.location.hash.slice(1));
47
+ const accessToken = hash.get('access_token');
48
+ const refreshToken = hash.get('refresh_token');
49
+ const nodeUrl = hash.get('node_url');
50
+ const appId = hash.get('application_id');
35
51
 
36
- const nodeUrl = getStorageAppEndpointKey();
37
- const jwt = getJWTObject();
38
- // jwt.access_token, jwt.refresh_token
52
+ if (!accessToken || !nodeUrl) return false;
53
+
54
+ // Store everything
55
+ setAppEndpointKey(nodeUrl);
56
+ setAccessToken(accessToken);
57
+ if (refreshToken) setRefreshToken(refreshToken);
58
+ if (appId) setApplicationId(appId);
59
+
60
+ // Extract contextId and executorPublicKey from JWT claims
61
+ setContextAndIdentityFromJWT(accessToken);
62
+
63
+ // Remove tokens from URL bar (don't let them sit in browser history)
64
+ history.replaceState(null, '', window.location.pathname + window.location.search);
65
+
66
+ return true;
67
+ }
39
68
  ```
40
69
 
41
- ## Checking auth status
70
+ ---
71
+
72
+ ## Manual login (no Desktop)
42
73
 
43
74
  ```typescript
44
- import { getJWTObject } from '@calimero-network/calimero-client';
75
+ import { setAppEndpointKey, setAccessToken, setRefreshToken, setContextAndIdentityFromJWT } from '@calimero-network/calimero-client';
45
76
 
46
- function isLoggedIn(): boolean {
47
- const jwt = getJWTObject();
48
- return !!(jwt?.access_token);
77
+ async function login(nodeUrl: string, accessToken: string, refreshToken: string) {
78
+ setAppEndpointKey(nodeUrl);
79
+ setAccessToken(accessToken);
80
+ setRefreshToken(refreshToken);
81
+ setContextAndIdentityFromJWT(accessToken);
49
82
  }
50
83
  ```
51
84
 
85
+ ---
86
+
87
+ ## Checking if authenticated
88
+
89
+ ```typescript
90
+ import { getAuthConfig } from '@calimero-network/calimero-client';
91
+
92
+ function isAuthenticated(): boolean {
93
+ const config = getAuthConfig();
94
+ return config.error === null;
95
+ }
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Reading the JWT payload
101
+
102
+ ```typescript
103
+ import { getJWTObject } from '@calimero-network/calimero-client';
104
+
105
+ const jwt = getJWTObject();
106
+ // jwt.context_id — the context this token is scoped to
107
+ // jwt.context_identity — the identity (executor public key)
108
+ // jwt.exp — expiry timestamp (seconds)
109
+ // jwt.permissions — array of permission strings
110
+ ```
111
+
112
+ ---
113
+
52
114
  ## Logout
53
115
 
54
116
  ```typescript
55
- localStorage.removeItem('calimero_jwt');
56
- localStorage.removeItem('calimero_node_url');
57
- // redirect to login
117
+ import { clientLogout } from '@calimero-network/calimero-client';
118
+
119
+ // Clears all tokens from localStorage and reloads the page
120
+ clientLogout();
121
+ ```
122
+
123
+ ---
124
+
125
+ ## App startup pattern (SSO + fallback)
126
+
127
+ ```typescript
128
+ import {
129
+ setAppEndpointKey, setAccessToken, setRefreshToken,
130
+ setApplicationId, setContextAndIdentityFromJWT,
131
+ getAuthConfig,
132
+ } from '@calimero-network/calimero-client';
133
+
134
+ async function bootstrap() {
135
+ // Try SSO from Desktop hash
136
+ const hash = new URLSearchParams(window.location.hash.slice(1));
137
+ const accessToken = hash.get('access_token');
138
+ const nodeUrl = hash.get('node_url');
139
+
140
+ if (accessToken && nodeUrl) {
141
+ setAppEndpointKey(nodeUrl);
142
+ setAccessToken(accessToken);
143
+ const rt = hash.get('refresh_token');
144
+ if (rt) setRefreshToken(rt);
145
+ const appId = hash.get('application_id');
146
+ if (appId) setApplicationId(appId);
147
+ setContextAndIdentityFromJWT(accessToken);
148
+ history.replaceState(null, '', window.location.pathname);
149
+ renderApp();
150
+ return;
151
+ }
152
+
153
+ // Check if already authenticated from a previous session
154
+ const config = getAuthConfig();
155
+ if (config.error === null) {
156
+ renderApp();
157
+ return;
158
+ }
159
+
160
+ // Not authenticated — show manual login
161
+ renderLogin();
162
+ }
58
163
  ```
@@ -2,74 +2,165 @@
2
2
 
3
3
  Calling application methods on a Calimero node.
4
4
 
5
- ## Setup
5
+ ## The `rpcClient` singleton
6
+
7
+ `@calimero-network/calimero-client` exports a pre-configured `rpcClient` singleton.
8
+ Import it directly — do not construct `JsonRpcClient` manually.
6
9
 
7
10
  ```typescript
8
11
  import {
9
- JsonRpcClient,
10
- getStorageAppEndpointKey,
11
- getJWTObject,
12
+ rpcClient,
13
+ getContextId,
14
+ getExecutorPublicKey,
12
15
  } from '@calimero-network/calimero-client';
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Execute a method (mutation or view — same call)
13
21
 
14
- const nodeUrl = getStorageAppEndpointKey() ?? 'http://localhost:2428';
15
- const jwt = getJWTObject();
22
+ ```typescript
23
+ const response = await rpcClient.execute<ArgsType, OutputType>({
24
+ contextId: getContextId()!,
25
+ method: 'methodName',
26
+ argsJson: { /* your args */ },
27
+ executorPublicKey: getExecutorPublicKey()!,
28
+ });
16
29
 
17
- const client = new JsonRpcClient(nodeUrl, '/jsonrpc', jwt?.access_token);
30
+ if (response.error) {
31
+ console.error(response.error.error.cause.info?.message);
32
+ } else {
33
+ console.log(response.result?.output);
34
+ }
18
35
  ```
19
36
 
37
+ ---
38
+
20
39
  ## Calling a mutation (changes state)
21
40
 
22
41
  ```typescript
23
- const response = await client.mutate<{ key: string; value: string }, void>({
24
- contextId: 'your-context-id',
42
+ const response = await rpcClient.execute<{ key: string; value: string }, void>({
43
+ contextId: getContextId()!,
25
44
  method: 'set',
26
45
  argsJson: { key: 'hello', value: 'world' },
27
- executorPublicKey: jwt?.executor_public_key ?? '',
46
+ executorPublicKey: getExecutorPublicKey()!,
28
47
  });
29
48
 
30
49
  if (response.error) {
31
- console.error(response.error);
50
+ console.error('set failed:', response.error.error.cause.info?.message);
32
51
  }
33
52
  ```
34
53
 
35
54
  ## Calling a view (read-only)
36
55
 
37
56
  ```typescript
38
- const response = await client.query<{ key: string }, string | null>({
39
- contextId: 'your-context-id',
57
+ const response = await rpcClient.execute<{ key: string }, string | null>({
58
+ contextId: getContextId()!,
40
59
  method: 'get',
41
60
  argsJson: { key: 'hello' },
42
- executorPublicKey: jwt?.executor_public_key ?? '',
61
+ executorPublicKey: getExecutorPublicKey()!,
43
62
  });
44
63
 
45
- console.log(response.result?.output); // "world"
64
+ if (!response.error) {
65
+ console.log(response.result?.output); // "world"
66
+ }
46
67
  ```
47
68
 
69
+ ---
70
+
48
71
  ## Response shape
49
72
 
50
73
  ```typescript
51
- interface RpcResponse<T> {
52
- result?: {
53
- output: T;
54
- };
55
- error?: {
56
- code: number;
57
- message: string;
58
- };
74
+ // Success:
75
+ { result: { output: T } }
76
+
77
+ // Error:
78
+ {
79
+ error: {
80
+ id: number;
81
+ jsonrpc: string;
82
+ code: number; // HTTP-like code (400, 401, 500)
83
+ error: {
84
+ name: string; // e.g. "FunctionCallError"
85
+ cause: {
86
+ name: string;
87
+ info?: { message: string };
88
+ };
89
+ };
90
+ headers?: Record<string, string>;
91
+ }
59
92
  }
60
93
  ```
61
94
 
62
- ## Error handling
95
+ ---
96
+
97
+ ## Error names
98
+
99
+ | name | meaning |
100
+ |---|---|
101
+ | `FunctionCallError` | App method returned an error |
102
+ | `RpcExecutionError` | Node couldn't execute the method |
103
+ | `InvalidRequestError` | Malformed request (wrong context-id, missing args) |
104
+ | `AuthenticationError` | JWT expired, revoked, or missing |
105
+ | `UnknownServerError` | Unexpected server error |
106
+
107
+ ---
108
+
109
+ ## Error handling with 401
110
+
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:
63
113
 
64
114
  ```typescript
65
- const response = await client.query({ ... });
115
+ const response = await rpcClient.execute({ ... });
66
116
 
67
117
  if (response.error) {
68
- if (response.error.code === 401) {
69
- // token expired — refresh and retry
70
- await refreshTokens();
71
- return callAgain();
118
+ const { code, headers } = response.error;
119
+ const authError = headers?.['x-auth-error'];
120
+
121
+ if (code === 401 && (authError === 'token_revoked' || authError === 'invalid_token')) {
122
+ // Token cannot be refreshed — send user to login
123
+ clientLogout();
124
+ return;
125
+ }
126
+
127
+ throw new Error(response.error.error.cause.info?.message ?? 'Unknown error');
128
+ }
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Complete example: typed wrapper
134
+
135
+ ```typescript
136
+ import {
137
+ rpcClient,
138
+ getContextId,
139
+ getExecutorPublicKey,
140
+ } from '@calimero-network/calimero-client';
141
+
142
+ async function setItem(key: string, value: string): Promise<void> {
143
+ const response = await rpcClient.execute<{ key: string; value: string }, void>({
144
+ contextId: getContextId()!,
145
+ method: 'set',
146
+ argsJson: { key, value },
147
+ executorPublicKey: getExecutorPublicKey()!,
148
+ });
149
+ if (response.error) {
150
+ throw new Error(response.error.error.cause.info?.message);
151
+ }
152
+ }
153
+
154
+ async function getItem(key: string): Promise<string | null> {
155
+ const response = await rpcClient.execute<{ key: string }, string | null>({
156
+ contextId: getContextId()!,
157
+ method: 'get',
158
+ argsJson: { key },
159
+ executorPublicKey: getExecutorPublicKey()!,
160
+ });
161
+ if (response.error) {
162
+ throw new Error(response.error.error.cause.info?.message);
72
163
  }
73
- throw new Error(response.error.message);
164
+ return response.result?.output ?? null;
74
165
  }
75
166
  ```
@@ -34,18 +34,29 @@ function readSSOParams(): {
34
34
 
35
35
  ## Using SSO tokens on app startup
36
36
 
37
+ Use the storage helpers from `@calimero-network/calimero-client`:
38
+
37
39
  ```typescript
40
+ import {
41
+ setAppEndpointKey,
42
+ setAccessToken,
43
+ setRefreshToken,
44
+ setApplicationId,
45
+ setContextAndIdentityFromJWT,
46
+ } from '@calimero-network/calimero-client';
47
+
38
48
  async function initApp() {
39
49
  const sso = readSSOParams();
40
50
 
41
51
  if (sso.accessToken && sso.nodeUrl) {
42
52
  // Opened from Desktop — store tokens and skip login
43
- localStorage.setItem('calimero_node_url', sso.nodeUrl);
44
- localStorage.setItem('calimero_jwt', JSON.stringify({
45
- access_token: sso.accessToken,
46
- refresh_token: sso.refreshToken,
47
- }));
48
- // clear hash so tokens aren't in browser history
53
+ setAppEndpointKey(sso.nodeUrl);
54
+ setAccessToken(sso.accessToken);
55
+ if (sso.refreshToken) setRefreshToken(sso.refreshToken);
56
+ if (sso.applicationId) setApplicationId(sso.applicationId);
57
+ // Extract contextId + executorPublicKey from the JWT claims
58
+ setContextAndIdentityFromJWT(sso.accessToken);
59
+ // Clear hash so tokens aren't in browser history
49
60
  history.replaceState(null, '', window.location.pathname);
50
61
  renderApp();
51
62
  } else {
@@ -55,12 +66,6 @@ async function initApp() {
55
66
  }
56
67
  ```
57
68
 
58
- ## calimero-client reads hash automatically
59
-
60
- If you use `@calimero-network/calimero-client`'s built-in auth helpers, the library
61
- reads `window.location.hash` automatically and stores the tokens. You don't need the
62
- manual parsing above unless you're building a custom auth flow.
63
-
64
69
  ## Important
65
70
 
66
71
  Always fall back to manual login if the hash params are absent — the app must work
@@ -2,65 +2,125 @@
2
2
 
3
3
  Subscribe to real-time events emitted by the application running on the node.
4
4
 
5
- ## Connect and subscribe
5
+ ## Event types
6
6
 
7
- ```typescript
8
- import { WsSubscriptionsClient } from '@calimero-network/calimero-client';
7
+ The node sends two kinds of events to subscribers:
8
+
9
+ | type | when | payload |
10
+ |---|---|---|
11
+ | `StateMutation` | Another member mutated shared state | `{ newRoot: string }` |
12
+ | `ExecutionEvent` | App emitted `app::emit!()` | `{ events: ExecutionEvent[] }` |
9
13
 
10
- const nodeUrl = getStorageAppEndpointKey() ?? 'http://localhost:2428';
11
- const jwt = getJWTObject();
14
+ An `ExecutionEvent` has: `{ kind: string; data: any }` where `kind` matches the Rust event variant name.
12
15
 
13
- const ws = new WsSubscriptionsClient(nodeUrl, '/ws', jwt?.access_token);
16
+ ---
17
+
18
+ ## Connect and subscribe
14
19
 
15
- ws.subscribe([contextId], (event) => {
20
+ ```typescript
21
+ import {
22
+ WsSubscriptionsClient,
23
+ getAppEndpointKey,
24
+ getContextId,
25
+ } from '@calimero-network/calimero-client';
26
+
27
+ const nodeUrl = getAppEndpointKey()!;
28
+ const ws = new WsSubscriptionsClient(nodeUrl, '/ws');
29
+
30
+ // Connect first, then subscribe and add callback
31
+ await ws.connect();
32
+ ws.subscribe([getContextId()!]);
33
+ ws.addCallback((event) => {
16
34
  console.log('Event received:', event);
17
- // event.data contains the serialized app event payload
18
35
  });
19
36
  ```
20
37
 
21
- ## Handling specific event types
38
+ ---
22
39
 
23
- ```typescript
24
- interface AppEvent {
25
- type: 'ItemAdded' | 'ItemRemoved' | 'MemberJoined';
26
- key?: string;
27
- value?: string;
28
- identity?: string;
29
- }
40
+ ## Handling ExecutionEvents from app logic
30
41
 
31
- ws.subscribe([contextId], (event) => {
32
- const appEvent = event.data as AppEvent;
42
+ ```typescript
43
+ import type {
44
+ NodeEvent,
45
+ ExecutionEventPayload,
46
+ StateMutationPayload,
47
+ } from '@calimero-network/calimero-client';
48
+
49
+ ws.addCallback((event: NodeEvent) => {
50
+ if (event.type === 'ExecutionEvent') {
51
+ 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
+ }
63
+ }
64
+ }
33
65
 
34
- switch (appEvent.type) {
35
- case 'ItemAdded':
36
- addItemToUI(appEvent.key!, appEvent.value!);
37
- break;
38
- case 'ItemRemoved':
39
- removeItemFromUI(appEvent.key!);
40
- break;
66
+ if (event.type === 'StateMutation') {
67
+ // Another member changed shared state — refresh data from node
68
+ console.log('State root changed:', event.data.newRoot);
69
+ refreshDataFromNode();
41
70
  }
42
71
  });
43
72
  ```
44
73
 
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
+
45
85
  ## Cleanup
46
86
 
47
87
  ```typescript
48
- // Unsubscribe when component unmounts
88
+ // Remove a specific callback
89
+ ws.removeCallback(myCallback);
90
+
91
+ // Unsubscribe from specific contexts
49
92
  ws.unsubscribe([contextId]);
50
93
 
51
- // Or close connection entirely
52
- ws.close();
94
+ // Close connection entirely
95
+ ws.disconnect();
53
96
  ```
54
97
 
98
+ ---
99
+
55
100
  ## React hook pattern
56
101
 
57
102
  ```typescript
58
- useEffect(() => {
59
- const ws = new WsSubscriptionsClient(nodeUrl, '/ws', token);
60
- ws.subscribe([contextId], handleEvent);
61
-
62
- return () => {
63
- ws.unsubscribe([contextId]);
64
- };
65
- }, [contextId, token]);
103
+ import { useEffect } from 'react';
104
+ import { WsSubscriptionsClient, getAppEndpointKey, getContextId } from '@calimero-network/calimero-client';
105
+
106
+ function useNodeEvents(onEvent: (event: NodeEvent) => void) {
107
+ useEffect(() => {
108
+ const nodeUrl = getAppEndpointKey();
109
+ const contextId = getContextId();
110
+ if (!nodeUrl || !contextId) return;
111
+
112
+ const ws = new WsSubscriptionsClient(nodeUrl, '/ws');
113
+
114
+ ws.connect().then(() => {
115
+ ws.subscribe([contextId]);
116
+ ws.addCallback(onEvent);
117
+ });
118
+
119
+ return () => {
120
+ ws.removeCallback(onEvent);
121
+ ws.unsubscribe([contextId]);
122
+ ws.disconnect();
123
+ };
124
+ }, [onEvent]);
125
+ }
66
126
  ```
@@ -10,25 +10,63 @@ You are helping a developer manage a **Calimero node** using `merod` and `meroct
10
10
  - **Application** — the WASM code; one app can power many contexts
11
11
  - Installing an app and creating a context are two separate steps
12
12
 
13
- ## Quick reference
13
+ ## Node setup (first time)
14
14
 
15
15
  ```bash
16
- # Start a node
16
+ # Initialize node configuration
17
+ merod --home ~/.calimero init
18
+
19
+ # Start the node
17
20
  merod --home ~/.calimero run
21
+ # Node listens on http://localhost:2428 by default
22
+ ```
23
+
24
+ ## Complete workflow: app → context → call
25
+
26
+ ```bash
27
+ # 1. Install an app
28
+ meroctl --node-url http://localhost:2428 app install \
29
+ --path myapp.mpk
30
+ # → prints app-id
31
+
32
+ # 2. Create a context (instantiate the app — init() is called)
33
+ meroctl --node-url http://localhost:2428 context create \
34
+ --app-id <app-id>
35
+ # → prints context-id
36
+
37
+ # 3. Call a mutation (changes state)
38
+ meroctl --node-url http://localhost:2428 call <context-id> set \
39
+ --args '{"key":"hello","value":"world"}'
18
40
 
19
- # Install an app from a .mpk bundle
20
- meroctl --node-url http://localhost:2428 app install --path myapp.mpk
41
+ # 4. Call a view (read-only)
42
+ meroctl --node-url http://localhost:2428 call <context-id> get \
43
+ --args '{"key":"hello"}' --view
21
44
 
22
- # Create a context (instance of an app)
23
- meroctl --node-url http://localhost:2428 context create --app-id <app-id>
45
+ # 5. Check node is running
46
+ meroctl --node-url http://localhost:2428 node health
47
+ ```
48
+
49
+ ## Multi-node context (invite + join)
24
50
 
25
- # List contexts
26
- meroctl --node-url http://localhost:2428 context ls
51
+ ```bash
52
+ # On node A invite a member
53
+ meroctl --node-url http://localhost:2428 context invite \
54
+ <context-id> --identity <identity-on-node-A>
55
+ # → prints invitation payload (JSON)
27
56
 
28
- # Call a method
29
- meroctl --node-url http://localhost:2428 call <context-id> <method> --args '{"key":"value"}'
57
+ # On node B — accept the invitation
58
+ meroctl --node-url http://localhost:2429 context join \
59
+ --invitation '<paste-invitation-payload>'
60
+ # → node B syncs state from node A
61
+ ```
62
+
63
+ ## Global flags
64
+
65
+ ```bash
66
+ meroctl --node-url http://localhost:2428 <command> # connect to specific node
67
+ meroctl --home ~/.calimero <command> # use alternate config path
30
68
  ```
31
69
 
32
70
  ## References
33
71
 
34
- See `references/` for node setup, full meroctl command reference, and context lifecycle.
72
+ See `references/` for full meroctl command reference and context lifecycle.
@@ -5,76 +5,179 @@ Full CLI for managing a running Calimero node.
5
5
  ## Global flags
6
6
 
7
7
  ```bash
8
- meroctl --node-url http://localhost:2428 <command>
9
- meroctl --home ~/.calimero <command> # alternate config path
8
+ meroctl --node-url http://localhost:2428 <command> # connect to specific node
9
+ meroctl --home ~/.calimero <command> # alternate config path
10
10
  ```
11
11
 
12
+ ---
13
+
14
+ ## Node commands
15
+
16
+ ```bash
17
+ # Check node health (returns "alive" when running)
18
+ meroctl --node-url http://localhost:2428 node health
19
+ ```
20
+
21
+ ---
22
+
12
23
  ## App commands
13
24
 
14
25
  ```bash
15
- # Install app from bundle
16
- meroctl app install --path myapp.mpk
26
+ # Install app from local bundle (.mpk or .wasm)
27
+ meroctl --node-url http://localhost:2428 app install --path myapp.mpk
28
+ meroctl --node-url http://localhost:2428 app install --path target/wasm32-unknown-unknown/release/myapp.wasm
17
29
 
18
- # Install app from registry
19
- meroctl app install --url https://registry.calimero.network/myapp/1.0.0
30
+ # Install app from registry URL
31
+ meroctl --node-url http://localhost:2428 app install \
32
+ --url https://registry.calimero.network/com.yourorg.myapp/1.0.0
20
33
 
21
34
  # List installed apps
22
- meroctl app ls
35
+ meroctl --node-url http://localhost:2428 app ls
23
36
 
24
- # Get app details
25
- meroctl app get <app-id>
37
+ # Get details of a specific app
38
+ meroctl --node-url http://localhost:2428 app get <app-id>
26
39
 
27
- # Remove app (only if no contexts use it)
28
- meroctl app remove <app-id>
40
+ # Remove an app (only works if no active contexts reference it)
41
+ meroctl --node-url http://localhost:2428 app remove <app-id>
29
42
  ```
30
43
 
44
+ ---
45
+
31
46
  ## Context commands
32
47
 
33
48
  ```bash
34
- # Create context (instance of an app)
35
- meroctl context create --app-id <app-id>
49
+ # Create a context (instantiates the app — calls init())
50
+ meroctl --node-url http://localhost:2428 context create --app-id <app-id>
51
+ # Returns: context-id
52
+
53
+ # List all contexts on this node
54
+ meroctl --node-url http://localhost:2428 context ls
55
+
56
+ # Get details of a specific context
57
+ meroctl --node-url http://localhost:2428 context get <context-id>
58
+
59
+ # Delete a context (wipes all state and storage for this context)
60
+ meroctl --node-url http://localhost:2428 context delete <context-id>
61
+
62
+ # List members of a context
63
+ meroctl --node-url http://localhost:2428 context members <context-id>
64
+
65
+ # Invite a member to a context (generates an invitation payload)
66
+ meroctl --node-url http://localhost:2428 context invite \
67
+ <context-id> --identity <identity>
68
+ # Returns: invitation payload JSON — share this with the invitee
69
+
70
+ # Join a context using an invitation payload (run on the joining node)
71
+ meroctl --node-url http://localhost:2428 context join \
72
+ --invitation '<invitation-payload-json>'
73
+ # After this, the node syncs state from the inviting node
74
+ ```
75
+
76
+ ### Full invite + join example
77
+
78
+ ```bash
79
+ # ── Node A (inviter) ──
80
+ meroctl --node-url http://localhost:2428 context invite \
81
+ abc123ctx --identity ed25519:AAAA...
82
+ # Prints: {"payload":"..."}
83
+
84
+ # ── Node B (joiner) ──
85
+ meroctl --node-url http://localhost:2429 context join \
86
+ --invitation '{"payload":"..."}'
87
+ # Node B now participates in the context and syncs CRDT state
88
+ ```
36
89
 
37
- # List contexts
38
- meroctl context ls
90
+ ---
39
91
 
40
- # Get context info
41
- meroctl context get <context-id>
92
+ ## Calling app methods
42
93
 
43
- # Delete context
44
- meroctl context delete <context-id>
94
+ ```bash
95
+ # Mutation changes shared state (no --view flag)
96
+ meroctl --node-url http://localhost:2428 call <context-id> <method> \
97
+ --args '{"key":"hello","value":"world"}'
45
98
 
46
- # Invite a member (generates invitation payload)
47
- meroctl context invite <context-id> --identity <identity>
99
+ # View read-only, does NOT change state
100
+ meroctl --node-url http://localhost:2428 call <context-id> <method> \
101
+ --args '{"key":"hello"}' --view
48
102
 
49
- # Join a context (accepts an invitation)
50
- meroctl context join --invitation <invitation-payload>
103
+ # Method with no arguments
104
+ meroctl --node-url http://localhost:2428 call <context-id> list_all \
105
+ --args '{}' --view
51
106
  ```
52
107
 
108
+ ---
109
+
53
110
  ## Identity commands
54
111
 
55
112
  ```bash
56
- # Create a new identity
57
- meroctl identity create
113
+ # Create a new identity (root keypair for this node)
114
+ meroctl --node-url http://localhost:2428 identity create
58
115
 
59
- # List identities
60
- meroctl identity ls
116
+ # List all identities on this node
117
+ meroctl --node-url http://localhost:2428 identity ls
61
118
 
62
- # Get identity details
63
- meroctl identity get <identity>
119
+ # Get details of a specific identity
120
+ meroctl --node-url http://localhost:2428 identity get <identity>
64
121
  ```
65
122
 
66
- ## Calling app methods
123
+ ---
124
+
125
+ ## Step-by-step: full local development flow
67
126
 
68
127
  ```bash
69
- # Mutation (changes state)
70
- meroctl call <context-id> set --args '{"key":"hello","value":"world"}'
128
+ # 1. Build the WASM app
129
+ cargo build --target wasm32-unknown-unknown --release
130
+
131
+ # 2. Start the node (in a separate terminal)
132
+ merod --home ~/.calimero run
133
+
134
+ # 3. Install the app
135
+ meroctl --node-url http://localhost:2428 app install \
136
+ --path target/wasm32-unknown-unknown/release/myapp.wasm
137
+ # Copy the app-id from output
71
138
 
72
- # View (read-only)
73
- meroctl call <context-id> get --args '{"key":"hello"}' --view
139
+ # 4. Create a context
140
+ meroctl --node-url http://localhost:2428 context create --app-id <app-id>
141
+ # Copy the context-id from output
142
+
143
+ # 5. Interact with the app
144
+ meroctl --node-url http://localhost:2428 call <context-id> set \
145
+ --args '{"key":"foo","value":"bar"}'
146
+
147
+ meroctl --node-url http://localhost:2428 call <context-id> get \
148
+ --args '{"key":"foo"}' --view
74
149
  ```
75
150
 
76
- ## Node health
151
+ ---
152
+
153
+ ## Step-by-step: multi-node context sharing
77
154
 
78
155
  ```bash
79
- meroctl node health
156
+ # ── Node A (port 2428) ──
157
+ # Install app and create context
158
+ meroctl --node-url http://localhost:2428 app install --path myapp.mpk
159
+ meroctl --node-url http://localhost:2428 context create --app-id <app-id>
160
+ # → <context-id>
161
+
162
+ # Create identity for node B to use
163
+ meroctl --node-url http://localhost:2428 identity create
164
+ # → <identity-b>
165
+
166
+ # Generate invitation
167
+ meroctl --node-url http://localhost:2428 context invite \
168
+ <context-id> --identity <identity-b>
169
+ # → <invitation-payload>
170
+
171
+ # ── Node B (port 2429) ──
172
+ # Accept invitation
173
+ meroctl --node-url http://localhost:2429 context join \
174
+ --invitation '<invitation-payload>'
175
+ # Node B syncs all existing state from node A
176
+
177
+ # Both nodes can now call methods and see each other's mutations:
178
+ meroctl --node-url http://localhost:2428 call <context-id> set \
179
+ --args '{"key":"shared","value":"data"}'
180
+ meroctl --node-url http://localhost:2429 call <context-id> get \
181
+ --args '{"key":"shared"}' --view
182
+ # → "data" (synced from node A)
80
183
  ```
@@ -12,19 +12,14 @@ You are helping a developer build a **Calimero WASM application** in Rust using
12
12
  - Private storage is per-member and isolated; shared state syncs across all members
13
13
  - There is no `main()` — the SDK provides the entry point
14
14
 
15
- ## Crate to add
15
+ ## Cargo.toml setup
16
16
 
17
- ```toml
18
- [dependencies]
19
- calimero-sdk = { path = "..." }
20
- # or from crates.io once published:
21
- calimero-sdk = "0.x"
22
- ```
23
-
24
- Also add to `Cargo.toml`:
25
17
  ```toml
26
18
  [lib]
27
19
  crate-type = ["cdylib"]
20
+
21
+ [dependencies]
22
+ calimero-sdk = "0.x"
28
23
  ```
29
24
 
30
25
  ## Minimal app skeleton
@@ -32,10 +27,11 @@ crate-type = ["cdylib"]
32
27
  ```rust
33
28
  use calimero_sdk::app;
34
29
  use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
30
+ use calimero_sdk::state::UnorderedMap;
35
31
 
36
32
  #[derive(Default, BorshDeserialize, BorshSerialize)]
37
33
  pub struct AppState {
38
- // use CRDT collections here
34
+ items: UnorderedMap<String, String>,
39
35
  }
40
36
 
41
37
  #[app::state]
@@ -48,24 +44,66 @@ impl AppState {
48
44
  AppState::default()
49
45
  }
50
46
 
51
- pub fn my_mutation(&mut self, value: String) -> Result<(), String> {
52
- // mutate state
47
+ pub fn set(&mut self, key: String, value: String) -> Result<(), String> {
48
+ self.items.insert(key, value);
53
49
  Ok(())
54
50
  }
55
51
 
56
- pub fn my_view(&self) -> String {
57
- // read-only
58
- String::new()
52
+ pub fn get(&self, key: String) -> Option<String> {
53
+ self.items.get(&key).cloned()
59
54
  }
60
55
  }
61
56
  ```
62
57
 
58
+ ## Building
59
+
60
+ ```bash
61
+ # Add WASM target (one-time)
62
+ rustup target add wasm32-unknown-unknown
63
+
64
+ # Build
65
+ cargo build --target wasm32-unknown-unknown --release
66
+
67
+ # Output: target/wasm32-unknown-unknown/release/<crate_name>.wasm
68
+ ```
69
+
70
+ ## Installing and running on a node (dev workflow)
71
+
72
+ ```bash
73
+ # 1. Install app from WASM file
74
+ meroctl --node-url http://localhost:2428 app install \
75
+ --path target/wasm32-unknown-unknown/release/myapp.wasm
76
+ # Returns: app-id
77
+
78
+ # 2. Create a context (instance of the app)
79
+ meroctl --node-url http://localhost:2428 context create --app-id <app-id>
80
+ # Returns: context-id
81
+
82
+ # 3. Call a method
83
+ meroctl --node-url http://localhost:2428 call <context-id> set \
84
+ --args '{"key":"hello","value":"world"}'
85
+
86
+ # 4. Call a view
87
+ meroctl --node-url http://localhost:2428 call <context-id> get \
88
+ --args '{"key":"hello"}' --view
89
+ ```
90
+
63
91
  ## Key rules
64
92
 
65
93
  - State struct **must** derive `Default`, `BorshDeserialize`, `BorshSerialize`
66
94
  - Never use `HashMap`, `Vec`, `BTreeMap` directly for state — use CRDT collections
67
95
  - No blocking I/O, no threads, no `async` in app logic
68
- - Use `calimero_sdk::env::log()` not `println!`
96
+ - Use `calimero_sdk::env::log!()` not `println!`
97
+ - Mutations use `&mut self`, views use `&self`
98
+
99
+ ## Logging
100
+
101
+ ```rust
102
+ use calimero_sdk::env;
103
+
104
+ // Inside any app method:
105
+ env::log!("Processing key: {}", key);
106
+ ```
69
107
 
70
108
  ## References
71
109