@calimero-network/agent-skills 0.3.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 -28
  3. package/package.json +1 -1
  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 +126 -31
  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 -92
  16. package/skills/calimero-client-js/rules/camelcase-api.md +10 -7
  17. package/skills/calimero-client-js/rules/token-refresh.md +11 -11
  18. package/skills/calimero-client-py/SKILL.md +25 -13
  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 +24 -19
  32. package/skills/calimero-desktop/references/sso-integration.md +2 -2
  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 +50 -39
  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 +9 -10
  71. package/skills/calimero-rust-sdk/rules/wasm-constraints.md +12 -10
  72. package/skills/calimero-sdk-js/SKILL.md +34 -26
  73. package/skills/calimero-sdk-js/references/build-pipeline.md +6 -6
  74. package/skills/calimero-sdk-js/references/collections.md +11 -11
  75. package/skills/calimero-sdk-js/references/events.md +7 -3
  76. package/skills/calimero-sdk-js/rules/crdt-only-state.md +18 -18
  77. package/skills/calimero-sdk-js/rules/no-console-log.md +6 -6
  78. package/skills/calimero-sdk-js/rules/view-decorator.md +6 -4
@@ -6,144 +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
+ }
26
44
 
27
- const nodeUrl = getAppEndpointKey()!;
28
- const ws = new WsSubscriptionsClient(nodeUrl, '/ws');
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
+ ```
29
57
 
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);
58
+ ## Subscribe to multiple contexts
59
+
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
- ## Connection errors and reconnection
101
-
102
- `WsSubscriptionsClient` does **not** reconnect automatically. If the connection drops
103
- or the initial connect fails, you must retry manually.
104
-
105
- ```typescript
106
- async function connectWithRetry(ws: WsSubscriptionsClient, contextId: string, retries = 3): Promise<void> {
107
- for (let i = 0; i < retries; i++) {
108
- try {
109
- await ws.connect();
110
- ws.subscribe([contextId]);
111
- return;
112
- } catch (err: any) {
113
- if (err?.status === 401) {
114
- // Token expired — cannot reconnect until re-authenticated
115
- clientLogout();
116
- return;
117
- }
118
- if (i === retries - 1) throw err;
119
- await new Promise(r => setTimeout(r, 1000 * (i + 1))); // exponential backoff
120
- }
121
- }
122
- }
123
- ```
124
-
125
- To detect drops after the connection is established, check if events stop arriving and
126
- reconnect periodically, or listen to `ws.onClose` if exposed.
127
-
128
- > WebSocket tokens are **not** auto-refreshed unlike `rpcClient` RPC calls.
129
- > See `rules/token-refresh.md` for details.
130
-
131
- ---
132
-
133
- ## React hook pattern
103
+ ## React hook for legacy client
134
104
 
135
105
  ```typescript
136
106
  import { useEffect } from 'react';
137
- import { WsSubscriptionsClient, getAppEndpointKey, getContextId } from '@calimero-network/calimero-client';
107
+ import {
108
+ WsSubscriptionsClient,
109
+ getAppEndpointKey,
110
+ getContextId,
111
+ } from '@calimero-network/calimero-client';
138
112
 
139
- function useNodeEvents(onEvent: (event: NodeEvent) => void) {
113
+ function useNodeEvents(onEvent: (event: any) => void) {
140
114
  useEffect(() => {
141
115
  const nodeUrl = getAppEndpointKey();
142
116
  const contextId = getContextId();
143
117
  if (!nodeUrl || !contextId) return;
144
118
 
145
119
  const ws = new WsSubscriptionsClient(nodeUrl, '/ws');
146
-
147
120
  ws.connect().then(() => {
148
121
  ws.subscribe([contextId]);
149
122
  ws.addCallback(onEvent);
@@ -157,3 +130,11 @@ function useNodeEvents(onEvent: (event: NodeEvent) => void) {
157
130
  }, [onEvent]);
158
131
  }
159
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
@@ -1,13 +1,14 @@
1
1
  # Rule: mero-js v2 uses camelCase — not snake_case
2
2
 
3
- `@calimero-network/mero-js` v2 (`>=2.0.0-beta.1`) changed all request object field names from `snake_case` to `camelCase`.
3
+ `@calimero-network/mero-js` v2 (`>=2.0.0-beta.1`) changed all request object field names from
4
+ `snake_case` to `camelCase`.
4
5
 
5
6
  ## WRONG (v1 / old code):
6
7
 
7
8
  ```typescript
8
9
  await client.generateClientKey({
9
- context_id: contextId, // ✗
10
- context_identity: identity, // ✗
10
+ context_id: contextId, // ✗
11
+ context_identity: identity, // ✗
11
12
  });
12
13
  ```
13
14
 
@@ -15,8 +16,8 @@ await client.generateClientKey({
15
16
 
16
17
  ```typescript
17
18
  await client.generateClientKey({
18
- contextId: contextId, // ✓
19
- contextIdentity: identity, // ✓
19
+ contextId: contextId, // ✓
20
+ contextIdentity: identity, // ✓
20
21
  });
21
22
  ```
22
23
 
@@ -27,11 +28,13 @@ await client.generateClientKey({
27
28
 
28
29
  ## Why this matters
29
30
 
30
- The v2 mero-js package does not alias the old snake_case keys. Passing `context_id` silently
31
- sends `undefined` to the server, which returns a cryptic error rather than a clear "wrong field name" message.
31
+ The v2 mero-js package does not alias the old snake_case keys. Passing `context_id` silently sends
32
+ `undefined` to the server, which returns a cryptic error rather than a clear "wrong field name"
33
+ message.
32
34
 
33
35
  ## How to detect if a codebase is on v1 or v2
34
36
 
35
37
  Check `package.json`:
38
+
36
39
  - `"@calimero-network/mero-js": "^2.0.0-beta.1"` or higher → v2, use camelCase
37
40
  - `"@calimero-network/calimero-client": "..."` → original client, check its docs
@@ -1,8 +1,8 @@
1
1
  # Rule: rpcClient handles token refresh automatically — but not all 401s
2
2
 
3
- `rpcClient` (from `@calimero-network/calimero-client`) automatically retries with a
4
- refreshed token when the server returns `401 token_expired`. **You do not need to
5
- implement a refresh wrapper around `rpcClient.execute()` calls.**
3
+ `rpcClient` (from `@calimero-network/calimero-client`) automatically retries with a refreshed token
4
+ when the server returns `401 token_expired`. **You do not need to implement a refresh wrapper around
5
+ `rpcClient.execute()` calls.**
6
6
 
7
7
  What you DO need to handle: 401s that cannot be refreshed.
8
8
 
@@ -49,8 +49,8 @@ if (response.error) {
49
49
 
50
50
  ## WebSocket connections are different — no auto-refresh
51
51
 
52
- `WsSubscriptionsClient` does **not** auto-refresh tokens. If the token expires while
53
- a WebSocket connection is open, events will stop arriving silently. Reconnect manually:
52
+ `WsSubscriptionsClient` does **not** auto-refresh tokens. If the token expires while a WebSocket
53
+ connection is open, events will stop arriving silently. Reconnect manually:
54
54
 
55
55
  ```typescript
56
56
  async function connectWithAuth() {
@@ -71,9 +71,9 @@ async function connectWithAuth() {
71
71
 
72
72
  ## Summary
73
73
 
74
- | Scenario | Handled by |
75
- | --- | --- |
76
- | `401 token_expired` on RPC call | `rpcClient` automatically (transparent retry) |
77
- | `401 token_revoked` on RPC call | You — redirect to login |
78
- | `401 invalid_token` on RPC call | You — redirect to login |
79
- | Auth failure on WebSocket connect | You — catch and redirect to login |
74
+ | Scenario | Handled by |
75
+ | --------------------------------- | --------------------------------------------- |
76
+ | `401 token_expired` on RPC call | `rpcClient` automatically (transparent retry) |
77
+ | `401 token_revoked` on RPC call | You — redirect to login |
78
+ | `401 invalid_token` on RPC call | You — redirect to login |
79
+ | Auth failure on WebSocket connect | You — catch and redirect to login |
@@ -1,19 +1,23 @@
1
1
  # calimero-client-py — Agent Instructions
2
2
 
3
- You are helping a developer use the **Calimero Python client** (`calimero-client-py`) to
4
- interact with a Calimero node from Python — automation scripts, backend services, or CLI tools.
3
+ You are helping a developer use the **Calimero Python client** (`calimero-client-py`) to interact
4
+ with a Calimero node from Python — automation scripts, backend services, or CLI tools.
5
5
 
6
- > **NOT this skill** if the developer is building the application logic that runs on the
7
- > node — that is `calimero-rust-sdk` (Rust/WASM) or `calimero-sdk-js` (TypeScript/WASM).
8
- > This skill is for Python code that *calls* the node from outside.
6
+ > **NOT this skill** if the developer is building the application logic that runs on the node — that
7
+ > is `calimero-rust-sdk` (Rust/WASM) or `calimero-sdk-js` (TypeScript/WASM). This skill is for
8
+ > Python code that _calls_ the node from outside.
9
9
 
10
10
  ## What it is
11
11
 
12
12
  `calimero-client-py` is a Python package built with PyO3 (Rust bindings). It provides:
13
- - Full async API for managing contexts, applications, identities, blobs
13
+
14
+ - Full API for managing contexts, applications, identities, blobs, namespaces, and groups
14
15
  - Automatic JWT token caching and refresh (tokens stored in `~/.merobox/auth_cache/`)
15
16
  - A CLI for common node operations
16
17
 
18
+ > **All methods are synchronous** — the Tokio runtime is managed internally by the Rust bindings. Do
19
+ > NOT use `await` with any client methods.
20
+
17
21
  ## Install
18
22
 
19
23
  ```bash
@@ -21,6 +25,7 @@ pip install calimero-client-py
21
25
  ```
22
26
 
23
27
  **Build from source (requires Rust 1.70+ and maturin):**
28
+
24
29
  ```bash
25
30
  pip install maturin
26
31
  git clone https://github.com/calimero-network/calimero-client-py
@@ -34,24 +39,31 @@ maturin develop --release
34
39
  import asyncio
35
40
  from calimero_client_py import create_connection, create_client
36
41
 
37
- async def main():
42
+ def main():
38
43
  connection = create_connection(
39
44
  api_url="http://localhost:2428",
40
45
  node_name="my-local-node" # must be stable — see rules/
41
46
  )
42
47
  client = create_client(connection)
43
- contexts = await client.list_contexts()
48
+ contexts = client.list_contexts() # no await — methods are synchronous
44
49
  print(contexts)
45
50
 
46
- asyncio.run(main())
51
+ main()
47
52
  ```
48
53
 
49
54
  ## Critical: `node_name` must be stable
50
55
 
51
- `node_name` is used to derive the token cache filename. If it changes between runs,
52
- the client loses its stored tokens and will need to re-authenticate.
56
+ `node_name` is used to derive the token cache filename. If it changes between runs, the client loses
57
+ its stored tokens and will need to re-authenticate.
58
+
59
+ ## Related skills
60
+
61
+ - **`calimero-core`** — context/app model and JSON-RPC protocol that the Python client wraps
62
+ - **`calimero-merod`** — `merod` daemon setup if the developer is also running the node
63
+ - **`calimero-meroctl`** — `meroctl` CLI reference for quick manual operations alongside Python
64
+ scripts
53
65
 
54
66
  ## References
55
67
 
56
- See `references/` for the full API, authentication flow, and CLI commands.
57
- See `rules/` for the `node_name` stability requirement and async usage.
68
+ See `references/` for the full API, authentication flow, and CLI commands. See `rules/` for the
69
+ `node_name` stability requirement and sync API usage.
@@ -1,5 +1,8 @@
1
1
  # Python Client API Reference
2
2
 
3
+ > All methods are **synchronous** — no `await` needed. The async Tokio runtime is managed internally
4
+ > by the Rust bindings.
5
+
3
6
  ## Setup
4
7
 
5
8
  ```python
@@ -16,52 +19,55 @@ client = create_client(connection)
16
19
 
17
20
  ```python
18
21
  # List all contexts
19
- contexts = await client.list_contexts()
22
+ contexts = client.list_contexts()
20
23
 
21
24
  # Get a specific context
22
- context = await client.get_context(context_id="abc123")
25
+ context = client.get_context(context_id="abc123")
23
26
 
24
27
  # Create a context (instance of an installed app)
25
- context = await client.create_context(
28
+ context = client.create_context(
26
29
  application_id="app-id",
27
30
  protocol="near", # "near" | "ethereum" | "icp" | "starknet"
28
31
  params=None # optional JSON string
29
32
  )
30
33
 
31
34
  # Delete a context
32
- await client.delete_context(context_id="abc123")
35
+ client.delete_context(context_id="abc123")
36
+
37
+ # Join a context (by context ID)
38
+ client.join_context(context_id="abc123")
33
39
 
34
40
  # Sync a context with peers
35
- await client.sync_context(context_id="abc123")
41
+ client.sync_context(context_id="abc123")
36
42
 
37
43
  # Sync all contexts
38
- await client.sync_all_contexts()
44
+ client.sync_all_contexts()
39
45
  ```
40
46
 
41
47
  ## Application management
42
48
 
43
49
  ```python
44
50
  # List installed apps
45
- apps = await client.list_applications()
51
+ apps = client.list_applications()
46
52
 
47
53
  # Get a specific app
48
- app = await client.get_application(app_id="app-id")
54
+ app = client.get_application(app_id="app-id")
49
55
 
50
56
  # Install from URL (registry or direct)
51
- result = await client.install_application(
57
+ result = client.install_application(
52
58
  url="https://registry.calimero.network/myapp/1.0.0",
53
59
  hash=None, # optional content hash for verification
54
60
  metadata=None # optional bytes
55
61
  )
56
62
 
57
63
  # Install from local path (dev mode)
58
- result = await client.install_dev_application(
64
+ result = client.install_dev_application(
59
65
  path="./target/wasm32-unknown-unknown/release/myapp.wasm",
60
66
  metadata=None
61
67
  )
62
68
 
63
69
  # Uninstall
64
- await client.uninstall_application(app_id="app-id")
70
+ client.uninstall_application(app_id="app-id")
65
71
  ```
66
72
 
67
73
  ## Calling app methods
@@ -70,7 +76,7 @@ await client.uninstall_application(app_id="app-id")
70
76
  import json
71
77
 
72
78
  # Execute a method (mutation or view)
73
- result = await client.execute_function(
79
+ result = client.execute_function(
74
80
  context_id="ctx-id",
75
81
  method="set",
76
82
  args=json.dumps({"key": "hello", "value": "world"}),
@@ -78,59 +84,51 @@ result = await client.execute_function(
78
84
  )
79
85
  ```
80
86
 
81
- ## Context membership
82
-
83
- ```python
84
- # Invite a member
85
- invitation = await client.invite_to_context(
86
- context_id="ctx-id",
87
- inviter_id="inviter-public-key",
88
- invitee_id="invitee-public-key"
89
- )
90
-
91
- # Join a context (on the invitee's node)
92
- await client.join_context(
93
- context_id="ctx-id",
94
- invitee_id="invitee-public-key",
95
- invitation_payload=invitation["payload"]
96
- )
97
- ```
98
-
99
87
  ## Identity management
100
88
 
101
89
  ```python
102
90
  # Generate a new context identity
103
- identity = await client.generate_context_identity()
91
+ identity = client.generate_context_identity()
104
92
  # Returns: { "public_key": "...", "private_key": "..." }
105
93
 
106
94
  # List identities for a context
107
- identities = await client.get_context_identities(context_id="ctx-id")
95
+ identities = client.get_context_identities(context_id="ctx-id")
108
96
 
109
97
  # Get client keys for a context
110
- keys = await client.get_context_client_keys(context_id="ctx-id")
98
+ keys = client.get_context_client_keys(context_id="ctx-id")
111
99
  ```
112
100
 
113
101
  ## Blob management
114
102
 
115
103
  ```python
116
- blobs = await client.list_blobs()
117
- info = await client.get_blob_info(blob_id="blob-id")
118
- await client.delete_blob(blob_id="blob-id")
104
+ blobs = client.list_blobs()
105
+ info = client.get_blob_info(blob_id="blob-id")
106
+ client.delete_blob(blob_id="blob-id")
107
+
108
+ # Upload bytes
109
+ result = client.upload_blob(data=b"hello", context_id=None)
110
+
111
+ # Download bytes
112
+ data = client.download_blob(blob_id="blob-id", context_id=None)
119
113
  ```
120
114
 
121
115
  ## Alias management
122
116
 
123
117
  ```python
124
118
  # Create aliases for easier reference
125
- await client.create_context_alias(alias="my-context", context_id="ctx-id")
126
- await client.create_application_alias(alias="my-app", application_id="app-id")
127
- await client.create_context_identity_alias(
119
+ client.create_context_alias(alias="my-context", context_id="ctx-id")
120
+ client.create_application_alias(alias="my-app", application_id="app-id")
121
+ client.create_context_identity_alias(
128
122
  context_id="ctx-id", alias="alice", public_key="pubkey"
129
123
  )
130
124
 
131
125
  # Delete aliases
132
- await client.delete_context_alias(alias="my-context")
133
- await client.delete_context_identity_alias(alias="alice", context_id="ctx-id")
126
+ client.delete_context_alias(alias="my-context")
127
+ client.delete_context_identity_alias(alias="alice", context_id="ctx-id")
128
+
129
+ # Lookup / resolve aliases
130
+ client.lookup_context_alias(alias="my-context")
131
+ client.resolve_context_alias(alias="my-context")
134
132
  ```
135
133
 
136
134
  ## Utility
@@ -140,8 +138,8 @@ await client.delete_context_identity_alias(alias="alice", context_id="ctx-id")
140
138
  url = client.get_api_url()
141
139
 
142
140
  # Get connected peers count
143
- peers = await client.get_peers_count()
141
+ peers = client.get_peers_count()
144
142
 
145
143
  # Get context storage info
146
- storage = await client.get_context_storage(context_id="ctx-id")
144
+ storage = client.get_context_storage(context_id="ctx-id")
147
145
  ```
@@ -1,11 +1,11 @@
1
1
  # Authentication & Token Caching
2
2
 
3
- The Python client automatically manages JWT tokens — loading from cache, using them in
4
- requests, and refreshing on 401.
3
+ The Python client automatically manages JWT tokens — loading from cache, using them in requests, and
4
+ refreshing on 401.
5
5
 
6
6
  ## Token cache location
7
7
 
8
- ```
8
+ ```text
9
9
  ~/.merobox/auth_cache/{sanitized_node_name}-{hash}.json
10
10
  ```
11
11
 
@@ -18,8 +18,8 @@ path = get_token_cache_path("my-node")
18
18
 
19
19
  ## First-time authentication
20
20
 
21
- On the first run there are no cached tokens. You need to complete an auth flow (login via
22
- the admin dashboard or meroctl) and then seed the cache manually:
21
+ On the first run there are no cached tokens. You need to complete an auth flow (login via the admin
22
+ dashboard or meroctl) and then seed the cache manually:
23
23
 
24
24
  ```python
25
25
  import json
@@ -54,8 +54,8 @@ client = create_client(connection)
54
54
 
55
55
  ## Token refresh
56
56
 
57
- The client automatically retries with a refreshed token on `401 Unauthorized`.
58
- Refreshed tokens are written back to the cache file automatically.
57
+ The client automatically retries with a refreshed token on `401 Unauthorized`. Refreshed tokens are
58
+ written back to the cache file automatically.
59
59
 
60
60
  ## CI / automated environments
61
61
 
@@ -1,52 +1,48 @@
1
- # Rule: All client methods are asyncmust be awaited
1
+ # Rule: All client methods are synchronousdo NOT use await
2
2
 
3
- All `client.*` methods return coroutines. They must be called with `await` inside an
4
- `async` function, and the event loop must be running.
3
+ Despite being built on an async Rust/Tokio runtime internally, all `client.*` methods are
4
+ **synchronous** from Python's perspective. Do NOT use `async`/`await`.
5
5
 
6
6
  ## WRONG:
7
7
 
8
8
  ```python
9
- # ✗ Missing await — returns a coroutine object, not the result
10
- contexts = client.list_contexts()
11
- print(contexts) # prints <coroutine object ...>
12
-
13
- # ✗ Calling outside async context
14
- def main():
15
- client.list_contexts() # won't run
9
+ # ✗ Adding await — methods are not coroutines
10
+ async def main():
11
+ contexts = await client.list_contexts() # TypeError: object is not awaitable
16
12
  ```
17
13
 
18
14
  ## CORRECT:
19
15
 
20
16
  ```python
21
- # ✓ Always await
22
- async def main():
23
- contexts = await client.list_contexts()
17
+ # ✓ Call methods directly — no await needed
18
+ def main():
19
+ conn = create_connection(api_url="http://localhost:2428", node_name="dev")
20
+ client = create_client(conn)
21
+ contexts = client.list_contexts() # synchronous, returns result directly
24
22
  print(contexts)
25
23
 
26
- asyncio.run(main())
24
+ main()
27
25
  ```
28
26
 
29
27
  ## In scripts
30
28
 
31
29
  ```python
32
- import asyncio
33
30
  from calimero_client_py import create_connection, create_client
34
-
35
- async def main():
36
- conn = create_connection(api_url="http://localhost:2428", node_name="dev")
37
- client = create_client(conn)
38
- result = await client.execute_function(
39
- context_id="ctx-id",
40
- method="get",
41
- args='{"key":"hello"}',
42
- executor_public_key="pubkey"
43
- )
44
- print(result)
45
-
46
- asyncio.run(main())
31
+ import json
32
+
33
+ conn = create_connection(api_url="http://localhost:2428", node_name="dev")
34
+ client = create_client(conn)
35
+
36
+ result = client.execute_function(
37
+ context_id="ctx-id",
38
+ method="get",
39
+ args=json.dumps({"key": "hello"}),
40
+ executor_public_key="pubkey"
41
+ )
42
+ print(result)
47
43
  ```
48
44
 
49
- ## Note: `create_connection` and `create_client` are synchronous
45
+ ## Note
50
46
 
51
- Only the API methods on the `client` object are async. The setup functions are regular
52
- synchronous calls.
47
+ The Tokio async runtime is embedded in the Rust bindings and blocks internally. From Python's view,
48
+ every call is a normal blocking function call.